【问题标题】:Why use std::forward in concepts?为什么在概念中使用 std::forward?
【发布时间】:2017-11-05 12:11:29
【问题描述】:

我正在阅读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&lt;T&gt;(t) 应该返回MyType&amp;&amp;,它不能传递给我的swap 函数。

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

【问题讨论】:

  • Concepts 真的要成为 c++1z 的一部分吗?
  • 不,不是。我太乐观了:)
  • 顺便说一句,内部错误似乎是由使用Swappable 作为模板约束引起的。如果用作要求,GCC 似乎能够正确处理它。或者更具体地说,void f(Swappable&amp; x) {}template&lt;Swappable S&gt; void f(S&amp; x) {} 都会导致内部错误,但 template&lt;typename S&gt; void f(S&amp; x) requires Swappable&lt;S&gt; {} 会编译(并发出错误,因为 std::forward 确实使它寻找右值引用版本)。
  • It seems to have that problem with any concept that takes two template parameters, with the second defaulting to the first (i.e., any concept with template&lt;typename T, typename U = T&gt;).在实际使用中遇到概念,在扩展概念的同时似乎无法正确推导参数,需要使用requires语法(这样一来参数在遇到概念之前已经推导出来,允许它从中推导出另一个)。
  • 这是 n4382 中的一个错字,已修复为使用转发参考

标签: c++ c++-concepts c++20


【解决方案1】:

我们不想用左值调用 swap […]

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

快速回顾一下可交换要求。也就是说,在今天的标准中已经出现并且在概念精简版之前就已经存在的实际要求:

  • 对象t可与对象u 交换当且仅当:
    • […] 表达式 swap(t, u)swap(u, t) 是有效的 […]

[…]

右值或左值 t 是可交换的当且仅当 t 可分别与 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&&> );

(现在从技术上讲,标准是在谈论对象而不是引用。由于引用不仅指对象或函数,而且旨在透明地代表它们,我们' ve 实际上提供了一个非常理想的功能。实际上,我们现在正在处理变量,而不仅仅是对象。)

这里有一个非常重要的联系:int&amp;&amp; 是我们变量的声明类型,以及传递给概念的实际参数,它最终再次成为我们的 lhs 和 @987654335 的声明类型@ 需要参数。在我们深入挖掘时请记住这一点。

Coliru demo

表达式

现在提到左值和右值的第二位呢?好吧,这里我们不再处理变量,而是处理表达式。我们可以为此写一个概念吗?好吧,我们可以使用某种表达式到类型的编码。即decltypestd::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&amp; 参数,它被恢复为std::declval&lt;int&amp;&gt;() 约束中的表达式。或者以更迂回的方式,std::forward&lt;int&amp;&gt;(lhs)

Coliru demo

把它放在一起

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

  • 我们可以将SecondKindOfSwappable 写成FirstKindOfSwappable,如下所示几乎

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

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

  • 限制swap(std::forward&lt;Lhs&gt;(lhs), std::forward&lt;Rhs&gt;(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&lt;Derived, Base&gt;(),它的值不会为您提供通常意义上的有效表达式(或使用变量的方法)。

事实上,例如Constructible&lt;Arg, Inits...&gt;() 第一个参数Arg 可以解释为两种方式:

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

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

我将以个人笔记结束:我认为读者不应该(目前)认为他们应该以相同的方式编写自己的概念,因为至少从概念作者的角度来看,概念优于表达式,是概念对变量的超集。

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

【讨论】:

  • 这一切都说得通,但 void f(Swappable&amp;);template &lt;Swappable S&gt; f(S&amp;); 没有按照我的意思行事仍然很烦人。我想我总是可以自己写template &lt;typename S, typename T=S&gt; concept bool LvalSwappable = Swappable&lt;S&amp;,T&amp;&gt;;
【解决方案2】:

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

这似乎是一个错字1,应该是requires(T&amp;&amp; t, U&amp;&amp; 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&amp;&amp;, U&amp;&amp;),因此这个简化版本也应该如此。因此,std::forwardtu 是转发引用的期望一起使用。

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


[以下内容扩展为Swappable。如果您不关心,请随意跳过它。]

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

但是请注意,简化形式本身并不是Swappable 的完整实现,除非已经指定了using std::swap[swappable.requirements/3] 声明重载解析必须同时考虑两个 std::swap() 模板和 ADL 找到的任何 swap()s(即,解析必须像使用声明 using std::swap 已指定一样继续进行)。由于概念不能包含使用声明,更完整的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&lt;(wildcard), (wildcard)&gt; 是否与模式template&lt;typename T, typename U&gt; 匹配(确实如此);然后它可以推断出T,并使用它来正确地确定它是否解析为Swappable 概念。

    相反,GCC 似乎已达到 Catch-22:它无法实例化 U,直到它推断出 T,但它无法推断出 T,直到它确定此 Swappable 是否正确解析为Swappable 的概念...没有 U 就离不开它。所以,它需要先弄清楚U是什么,然后才能知道我们是否拥有正确的Swappable,但它需要知道我们是否拥有正确的Swappable,然后才能知道U是什么;面对这个无法解决的难题,它长了一个动脉瘤,倒了下去,死了。

【讨论】:

  • tu 真的是转发引用吗? TU 不是函数模板的模板参数...
  • 我从早期的 Ranges TS 草案中按原样采用了 cppreference 的可交换概念。我不记得当时是哪个 Ranges 版本,但 n4382 是它以这种方式出现的一个例子(在 [concepts.lib.corelang.swappable] 中)。
  • 在概念定义中将T t 替换为T&amp;&amp; t 似乎没有太大变化。 t 仍然是 std::remove_reference_t&lt;T&gt; 类型的左值。 decltype(t) 可能不同,但未使用。所以基本点是Swappable 是用来引用的,而不是标量类型?这是非常不直观的。用简单的英语,我们说intstd::string 是“可交换的”,而不是对intstd::string 的引用。
  • 另外,如果我没记错的话,这些语义会使使用短模板函数形式变得更加困难。我认为void f(Swappable&amp;&amp;); 应该可以工作并且通常只取左值,但它看起来很奇怪。
  • @aschepler 他们似乎至少在转发引用;不过,我不确定这实际上是由 Concepts TS 指定的,还是只是 GCC 实现的一部分。根据 CPPReference 页面,requires-expressionparameter-list 是“一个逗号分隔的参数列表,就像在函数声明中一样”;这表明他们使用函数模板推导规则,这将允许转发引用。 std::forward 的使用也表明requires 使用转发引用,as does testing
猜你喜欢
  • 2014-12-20
  • 1970-01-01
  • 2021-02-20
  • 2021-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多