c++ std::declval是否因为保证副本省略而过时?

bttbmeg0  于 2023-07-01  发布在  其他
关注(0)|答案(3)|浏览(127)

标准库实用程序declvaldefined,如下所示:

template<class T> add_rvalue_reference_t<T> declval() noexcept;

在这里添加一个 * 右值引用 * 似乎是一个好主意,如果你想一下它在 C++11 中引入时的语言:返回一个值涉及到一个临时的,它随后被从.现在 C++17 引入了 * 保证副本省略 *,这不再适用。正如cppref所说:
C17核心语言的纯右值和临时值规范与早期的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类型trait表明这是不可能的,因为它是根据declval定义的:
模板特化is_­constructible<T, Args...>的 predicate 条件应满足,当且仅当以下变量定义对于某个发明的变量t是良构的:
T t(declval<Args>()...);
因此,在我们的示例中,trait类型声明if Class可以从一个返回Class&&的假设函数构造。test()中的行是否被允许不能被任何当前的类型trait预测,尽管命名表明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;

这是C17中的一个缺陷(以及随后的,即C20)标准?或者我错过了一点为什么这些declvalis_constructibleis_invocable规范仍然是我们可以拥有的最佳解决方案?

azpvetkf

azpvetkf1#

然而,is_construtible类型trait表明这是不可能的,因为它是根据declval定义的:
Class不能从它自己类型的示例中构造。所以is_constructible不应该说它是。
如果一个类型T满足is_constructible<T, T>,则期望你可以在给定一个类型为T的对象的情况下生成一个T,* 而不是 * 你可以从一个类型为T的纯右值生成一个T。这不是使用declval的怪癖;这就是问题is_constructible的意思。
你的建议是is_constructible应该回答一个与它打算回答的问题不同的问题。应该注意的是,保证省略意味着 * 所有类型 * 都可以从它自己类型的纯右值“构造”。如果这是你想问的,你已经有答案了。

xcitsw88

xcitsw882#

std::declval函数主要用于转发。下面是一个例子:

template<typename... Ts>
auto f(Ts&&... args) -> decltype(g(std::declval<Ts>()...)) {
    return g(std::forward<Args>(args)...);
}

在这种常见的情况下,让std::declval返回纯右值是错误的,因为没有好的方法来转发纯右值。

km0tfn4u

km0tfn4u3#

在C++23中,随着std::reference_converts_from_temporary/std::reference_constructs_from_temporary的增加,T是纯右值,T&是左值,T&&是类型trait中的xvalue。
它在标准中定义为VAL<T>,如果T是引用类型,则基本上是declval<T>(),否则是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&&>(),但现在不能更改。

相关问题