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

Why use std::forward in concepts?

我正在阅读 cppreference page on Constraints 并注意到这个例子:

// 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));
};

我很困惑他们为什么要使用 std::forward。有些人试图在模板参数中支持引用类型?我们不想用左值调用 swap,当 TU 是标量(非引用)类型时,forward 表达式不是右值吗?

例如,考虑到他们的 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);
}

不幸的是,g++ 7.1.0 给了我一个 internal compiler error,这并没有说明这一点。

这里TU都应该是MyTypestd::forward<T>(t)应该是returnMyType&&,不能传到我的 swap 函数。

这个Swappable的实现有错吗?我错过了什么吗?

我对概念还是很陌生,所以请随时指出我需要在此答案中修复的任何错误。答案分为三段:第一段直接关于std::forward的使用,第二段关于Swappable的扩展,第三段关于内部错误

这似乎是一个拼写错误1,可能应该是 requires(T&& t, U&& u)。在这种情况下,完美转发用于确保概念将被正确评估左值和右值引用,保证只有左值引用将被标记为可交换。

完整的 Ranges TS Swappable concept,这是基于的,完全定义为:

template <class T>
concept bool Swappable() {
    return requires(T&& a, T&& b) {
               ranges::swap(std::forward<T>(a), std::forward<T>(b));
           };
}

template <class T, class U>
concept bool Swappable() {
    return ranges::Swappable<T>() &&
           ranges::Swappable<U>() &&
           ranges::CommonReference<const T&, const U&>() &&
           requires(T&& t, U&& u) {
               ranges::swap(std::forward<T>(t), std::forward<U>(u));
               ranges::swap(std::forward<U>(u), std::forward<T>(t));
           };
}

Constraints and concepts 页面上显示的概念是此概念的简化版本,似乎旨在作为库概念 Swappable 的最小实现。由于完整定义指定 requires(T&&, U&&),按理说这个简化版本也应该如此。因此,std::forward 的使用期望 tu 是转发引用。

1: ,我在测试代码、做研究和吃晚饭时制作的,确认这是一个打字错误。


[以下对Swappable进行了扩展。如果这与您无关,请随时跳过它。]

请注意,此部分仅适用于 Swappable 定义在命名空间 std 之外的情况;如果在 std 中定义,就像在 the draft 中一样,两个 std::swap() 将在重载解析期间自动考虑,这意味着不需要额外的工作来包含它们。感谢 Cubbi 链接到草稿并指出 Swappable 直接取自它。

但是请注意,简化形式本身并不是 Swappable 的完整实现,除非已经指定了 using std::swap[swappable.requirements/3] 声明重载解析必须同时考虑两个 std::swap() 模板和 ADL 找到的任何 swap()(即,解析必须像使用声明 using std::swap 一样进行指定的)。由于概念不能包含 using 声明,更完整的 Swappable 可能看起来像这样:

template<typename T, typename U = T>
concept bool ADLSwappable = requires(T&& t, U&& u) {
    swap(std::forward<T>(t), std::forward<U>(u));
    swap(std::forward<U>(u), std::forward<T>(t));
};

template<typename T, typename U = T>
concept bool StdSwappable = requires(T&& t, U&& u) {
    std::swap(std::forward<T>(t), std::forward<U>(u));
    std::swap(std::forward<U>(u), std::forward<T>(t));
};

template<typename T, typename U = T>
concept bool Swappable = ADLSwappable<T, U> || StdSwappable<T, U>;

此扩展 Swappable 将允许正确检测满足库概念的参数,like so


[以下为GCC内部错误,与Swappable本身无直接关系。如果这与您无关,请随时跳过它。]

但是,要使用它,f() 需要进行一些修改。而不是:

void f(Swappable& x) {}

应改为使用以下之一:

template<typename T>
void f(T&& x) requires Swappable<T&&> {}

template<typename T>
void f(T& x) requires Swappable<T&> {}

这是由于 GCC 和概念解析规则之间的相互作用,并且可能会在未来版本的编译器中得到解决。使用约束表达式回避了我认为导致内部错误的交互,使其成为暂时可行的(如果更冗长的话)权宜之计。

内部错误似乎是由 GCC 处理概念解析规则的方式引起的。当它遇到这个函数时:

void f(Swappable& x) {}

由于函数概念可以重载,当在某些上下文中遇到概念名称时(例如用作约束类型说明符时,如此处的Swappable),将执行概念解析。因此,GCC 尝试按照概念解析规则 #1 in the Concept resolution section of this page:

的规定解析 Swappable
  1. 由于在没有参数列表的情况下使用 Swappable,因此它采用单个通配符作为其参数。此通配符可以匹配任何可能的模板参数(无论是类型、非类型还是模板),因此是 T.

    的完美匹配
  2. 由于Swappable的第二个参数不对应参数,将使用其默认模板参数,如编号规则后指定;我相信这是问题所在。由于 T 目前是 (wildcard),一种简单的方法是暂时将 U 实例化为另一个通配符或第一个通配符的副本,并确定 Swappable<(wildcard), (wildcard)> 是否与模式匹配template<typename T, typename U>(确实如此);然后它可以推断 T,并使用它来正确确定它是否解析为 Swappable 概念。

    相反,GCC 似乎达到了第 22 条军规:在推导 T 之前无法实例化 U,但在确定之前无法推导 T这个 Swappable 是否正确地解析为 Swappable 概念...没有 U 它就做不到。所以,它需要先弄清楚U是什么,然后才能算出我们是否有权利Swappable,但它需要知道我们是否有权利Swappable,才能算出U 是什么;面对这个无法解决的难题,它得了动脉瘤,倒下,死了。

Don't we want to call swap with lvalues […]

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

对可交换要求的快速回顾。也就是说,实际要求已经出现在今天的标准中,并且在 concepts-lite 之前就已经存在了:

  • An object t is swappable with an object u if and only if:
    • […] the expressions swap(t, u) and swap(u, t) are valid […]

[…]

An rvalue or lvalue t is swappable if and only if t is swappable with any rvalue or lvalue, respectively, of type T.

(摘自 Swappable requirements [swappable.requirements] 以减少大量不相关的细节。)

变量

你明白了吗?第一位给出符合您期望的要求。变成一个实际的概念也很简单†:

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

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

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

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&&> );

(从技术上讲,标准是指对象,而引用不是。因为引用不仅指对象 或函数 ,而且意在t运行sparently 代表他们,我们实际上提供了一个非常理想的功能。实际上,我们现在正在根据 variables,而不仅仅是对象。)

这里有一个非常重要的联系:int&& 是我们变量的声明类型,也是传递给概念的实际参数,它又作为我们的 [= 的声明类型结束24=] 和 rhs 需要参数。在我们深入挖掘时请记住这一点。

Coliru demo

表达式

现在提到左值和右值的第二位怎么样?好吧,这里我们不再处理变量,而是根据 表达式 。我们可以为此写一个概念吗?好吧,我们可以使用某种表达式到类型的编码。即 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>());
};

这就是你运行喜欢的!正如您所发现的,这个概念必须以不同的方式使用:

// 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&> );

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

Coliru demo

放在一起

出现在 cppreference 条目上的是 Ranges TS 指定的 Swappable 概念的摘要。如果我猜的话,我会说 Ranges TS 决定使用 Swappable 参数来代表表达式,原因如下:

  • 我们可以根据 FirstKindOfSwappable 写出 SecondKindOfSwappable 如下 nearly:

    template<typename Lhs, typename Rhs = Lhs>
    concept bool FirstKindOfSwappable = SecondKindOfSwappable<Lhs&, Rhs&>;
    

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

  • 限制 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);
    }
    
  • 一致性:在大多数情况下,TS 的其他概念遵循相同的约定,并根据表达式类型进行参数化

但为什么大多数情况下?

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

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

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

我应该如何写自己的概念?

我将以个人说明作为结尾:我认为 reader 不应该(还)得出结论说他们应该以相同的方式编写自己的概念只是因为出现了概念而不是表达式,至少从角度来看概念作者,成为变量概念的超集。

还有其他因素在起作用,我关心的是从概念客户的角度来看的可用性以及所有这些我只是顺便提到的细节。但这与问题无关,而且这个答案已经足够长了,所以我将把这个故事留到下一次。