为什么在概念中使用std :: forward?

asc*_*ler 22 c++ c++-concepts c++20

我正在阅读Constraints上cppreference页面并注意到这个例子:

// example constraint from the standard library (ranges TS)
template <class T, class U = T>
concept bool Swappable = requires(T t, U u) {
    swap(std::forward<T>(t), std::forward<U>(u));
    swap(std::forward<U>(u), std::forward<T>(t));
};
Run Code Online (Sandbox Code Playgroud)

我很困惑他们为什么要用std::forward.有些人试图在模板参数中支持引用类型吗?我们不想swapforward左值调用,并且当标量(非参考)类型时T,表达式不是rvalues U吗?

例如,我希望这个程序在Swappable实现时失败:

#include <utility>

// example constraint from the standard library (ranges TS)
template <class T, class U = T>
concept bool Swappable = requires(T t, U u) {
    swap(std::forward<T>(t), std::forward<U>(u));
    swap(std::forward<U>(u), std::forward<T>(t));
};

class MyType {};
void swap(MyType&, MyType&) {}

void f(Swappable& x) {}

int main()
{
    MyType x;
    f(x);
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,g ++ 7.1.0给了我一个内部编译器错误,但这并未对此有所了解.

这里TU都应该MyType,并且std::forward<T>(t)应该返回MyType&&,这不能传递给我的swap函数.

这种执行是Swappable错误的吗?我错过了什么吗?

Luc*_*ton 5

我们不想用左值调用交换[...]

这是一个非常好的问题.API设计的一个问题:概念库的设计者应该赋予其概念参数什么含义或含义?

快速回顾一下可交换的需求.也就是说,已经出现在今天的标准中的实际要求已经出现在概念之前:

  • 一个对象t交换与对象u当且仅当:
    • [...]表达式swap(t, u)swap(u, t)有效[...]

[...]

一个rvalue或左值t是可交换的,当且仅当t是可交换与类型的任何右值或左值分别T.

(摘录来自Swappable要求[swappable.requirements],以减少大量不相关的细节.)

变量

你抓到了吗?第一位提供符合您期望的要求.变成一个实际的概念†也很简单:

†:只要我们愿意忽略大量超出我们范围的细节

template<typename Lhs, typename Rhs = Lhs>
concept bool FirstKindOfSwappable = requires(Lhs lhs, Rhs rhs) {
    swap(lhs, rhs);
    swap(rhs, lhs);
};
Run Code Online (Sandbox Code Playgroud)

现在,非常重要的是我们应该立即注意到这个概念支持开箱即用的参考变量:

int&& a_rref = 0;
int&& b_rref = 0;
// valid...
using std::swap;
swap(a_rref, b_rref);
// ...which is reflected here
static_assert( FirstKindOfSwappable<int&&> );
Run Code Online (Sandbox Code Playgroud)

(现在从技术上来说,标准是根据参考文献进行讨论而不是参考文献.由于参考文献不仅涉及对象或功能,而且意味着透明地代表它们,我们实际上提供了一个非常理想的特征.实际上我们现在是在变量方面工作,而不仅仅是对象.)

这里有一个非常重要的连接:int&&是我们变量的声明类型,以及传递给概念的实际参数,而后者又作为我们的声明类型lhsrhs需要参数再次结束.随着我们深入挖掘,请记住这一点.

Coliru 演示

表达式

那么提到左值和右值的那第二位呢?好吧,这里我们不再处理变量,而是表达式.我们能为此写一个概念吗?好吧,我们可以使用某种表达式到类型的编码.即一个由所用decltype以及std::declval在另一个方向上.这导致我们:

template<typenaome Lhs, typename Rhs = Lhs>
concept bool SecondKindOfSwappable = requires(Lhs lhs, Rhs rhs) {
    swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs));
    swap(std::forward<Rhs>(rhs), std::forward<Lhs>(lhs));

    // another way to express the first requirement
    swap(std::declval<Lhs>(), std::declval<Rhs>());
};
Run Code Online (Sandbox Code Playgroud)

这是你遇到的!正如您所发现的那样,必须以不同的方式使用该概念:

// not valid
//swap(0, 0);
//     ^- rvalue expression of type int
//        decltype( (0) ) => int&&
static_assert( !SecondKindOfSwappable<int&&> );
// same effect because the expression-decltype/std::declval encoding
// cannot properly tell apart prvalues and xvalues
static_assert( !SecondKindOfSwappable<int> );

int a = 0, b = 0;
swap(a, b);
//   ^- lvalue expression of type int
//      decltype( (a) ) => int&
static_assert( SecondKindOfSwappable<int&> );
Run Code Online (Sandbox Code Playgroud)

如果你发现不明显的话,那么看看这次播放时的连接:我们有一个类型的左值表达式int,它被编码为int&概念的参数,它被恢复为约束中的表达式std::declval<int&>().或者以更迂回的方式,通过std::forward<int&>(lhs).

Coliru 演示

把它放在一起

cppreference条目上显示的内容是SwappableRanges TS指定的概念的摘要.如果我猜测,我会说Ranges TS决定给出Swappable参数表示表达式,原因如下:

  • 我们可以写SecondKindOfSwappable在条款FirstKindOfSwappable由以下给出:

    template<typename Lhs, typename Rhs = Lhs>
    concept bool FirstKindOfSwappable = SecondKindOfSwappable<Lhs&, Rhs&>;
    
    Run Code Online (Sandbox Code Playgroud)

    这个配方可以应用于许多但不是所有情况,使得有时可以根据在表达式隐藏类型中参数化的相同概念来表达在变量类型上参数化的概念.但通常不可能采取相反的方式.

  • swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs))预计限制将是一个非常重要的场景; 在我的头脑中,它出现在以下业务中:

    template<typename Val, typename It>
    void client_code(Val val, It it)
        requires Swappable<Val&, decltype(*it)>
    //                           ^^^^^^^^^^^^^--.
    //                                          |
    //  hiding an expression into a type! ------`
    {
        ranges::swap(val, *it);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 一致性:在大多数情况下,TS的其他概念遵循相同的约定,并且针对表达式的类型进行参数化

但为什么大部分?

因为有第三种概念参数:代表...类型的类型.一个很好的例子是DerivedFrom<Derived, Base>(),在通常意义上,哪个值不能为您提供有效的表达式(或使用变量的方法).

事实上,例如Constructible<Arg, Inits...>(),第一个论证Arg可以用两种方式解释:

  • Arg 代表一种类型,即将可构造性作为一种类型的固有属性
  • Arg是正在构造的变量的声明类型,即约束暗示Arg imaginary_var { std::declval<Inits>()... };有效

我该如何写自己的概念?

最后我将以个人的方式结束:我认为读者不应该(尽管)他们应该以相同的方式编写自己的概念,因为表达式上的概念至少从概念编写者的角度出现,是一个超集变量的概念.

还有其他因素在起作用,我关注的是从概念客户的角度来看可用性以及我刚才提到的所有这些细节.但这并不是真的与这个问题有关,这个答案已经足够长了,所以我会把这个故事留到另一个时间.