标准库实用程序declval
定义为:
template<class T> add_rvalue_reference_t<T> declval() noexcept;
如果您考虑一下C++11中引入的语言,在这里添加rvalue 引用似乎是一个好主意:返回值涉及一个临时值,随后从中移动。 现在C++17引入了保证复制省略,这不再适用。正如cppref所说:
C++17 prvalues 和临时性的核心语言规范是 与前C++修订版的根本不同:有 不再是临时复制/移动。另一种描述方式 C++17 力学是"非物质化的价值传递":价值是 返回并使用,而无需实现临时。
这对在declval
方面实施的其他实用程序产生了一些影响。看看这个例子(查看 godbolt.org):
#include <type_traits>
struct Class {
explicit Class() noexcept {}
Class& operator=(Class&&) noexcept = delete;
};
Class getClass() {
return Class();
}
void test() noexcept {
Class c{getClass()}; // succeeds in C++17 because of guaranteed copy elision
}
static_assert(std::is_constructible<Class, Class>::value); // fails because move ctor is deleted
这里我们有一个不可移动的类。由于保证了复制省略,它可以从函数返回,然后在test()
中本地实现。然而,is_construtible
型特征表明这是不可能的,因为它是根据declval
定义的:
模板专用化的谓词条件
is_constructible<T, Args...>
应得到满足,当且仅当 以下变量定义对于某些发明的格式很好 可变t
:T t(declval<Args>()...);
因此,在我们的示例中,类型特征声明是否可以从返回Class&&
的假设函数构造Class
。当前任何类型特征都无法预测test()
中的行是否允许,尽管命名表明is_constructible
确实如此。
这意味着,在所有保证复制省略实际上可以挽救这一天的情况下,is_constructible
误导我们,告诉我们"它在C++11 中是可构造的吗?"的答案。
这不仅限于is_constructible
.扩展上面的示例(查看 godbolt.org)
void consume(Class) noexcept {}
void test2() {
consume(getClass()); // succeeds in C++17 because of guaranteed copy elision
}
static_assert(std::is_invocable<decltype(consume), Class>::value); // fails because move ctor is deleted
这表明is_invocable
也受到类似的影响。
对此最直接的解决方案是将declval
更改为
template<class T> T declval_cpp17() noexcept;
这是 C++17(及后续,即 C++20)标准的缺陷吗?还是我错过了为什么这些declval
、is_constructible
和is_invocable
规格仍然是我们可以拥有的最佳解决方案?
然而,
is_construtible
类型特征表明这是不可能的,因为它是根据declval
定义的:
Class
不能从其自身类型的实例构造。所以is_constructible
不应该说它是。
如果一个类型T
满足is_constructible<T, T>
,则期望是你可以为给定类型T
的对象创建一个T
,而不是你可以专门从类型T
的prvalue创建一个T
。这不是使用declval
的怪癖;这就是is_constructible
问题的含义。
您的建议是,is_constructible
应该回答一个与它打算回答的问题不同的问题。应该注意的是,保证省略意味着所有类型都可以从其自身类型的 prvalue "构造"。因此,如果这是您想问的,那么您已经有了答案。
std::declval
函数主要用于转发。下面是一个示例:
template<typename... Ts>
auto f(Ts&&... args) -> decltype(g(std::declval<Ts>()...)) {
return g(std::forward<Args>(args)...);
}
在这种常见情况下,std::declval
返回 prvalue 是错误的,因为没有转发 prvalue 的好方法。
在 C++23 中,随着std::reference_converts_from_temporary
/std::reference_constructs_from_temporary
的加入,现在优先将T
为prvalue,T&
为左值,T&&
为类型特征中的xvalue。
它在标准中以VAL<T>
来定义,如果T
是引用类型,否则是T
类型的prvalue,则基本上是declval<T>()
。
这个VAL<T>
declval_cpp17<T>()
非常相似。这在您提到的示例中很有用。
但是,std::is_constructible_v
的定义永远不能更改为是否编译T t(VAL<Args>...);
。现有呼叫站点过多,如下所示:
template<typename... Args> requires(std::is_constructible_v<T, Args...>)
void construct(Args&&... args);
必须将其更改为is_constructible_v<T, Args&&...>
才能正确。
我相信add_rvalue_reference_t
存在于declval
中,因为它旨在与完美转发一起使用。 即,std::forward<T>(expr)
将具有与std::declval<T>()
相同的类型和值类别。这应该一直std::declval<T&&>()
,但现在无法更改。