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

时间:2017-06-04 17:13:39

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

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

var gpsProperties: [(NSString, String)] = [
    ("exif:GPSLongitude", "\(abs(coordinate.longitude))"),
    ("exif:GPSLongitudeRef", coordinate.longitude < 0 ? "W" : "E"),
    ("exif:GPSLatitude", "\(abs(coordinate.latitude))"),
    ("exif:GPSLatitudeRef", coordinate.latitude < 0 ? "S" : "N")]

我很困惑他们为什么要使用// 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,当swapforward是标量(非参考)类型时,T表达式不是rvalues吗?< / p>

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

Swappable

不幸的是,g ++ 7.1.0给了我一个internal compiler error,但这并未对此有所了解。

此处#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); } T都应该是U,而MyType应该返回std::forward<T>(t),这不能传递给我的MyType&&功能。

swap的实施是否错误?我错过了什么吗?

2 个答案:

答案 0 :(得分:5)

  

我们不想用左值[...]

调用swap

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

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

  
      
  • 对象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&&> );

(从技术上来说,标准是根据引用的对象进行讨论。因为引用不仅指对象或函数,而且意味着透明地代表它们,我们实际上已经提供了一个非常理想的功能。实际上我们现在正在使用变量,而不仅仅是对象。)

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

Coliru demo

表达式

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

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

Coliru demo

把它放在一起

cppreference条目上显示的内容是Ranges TS指定的std::forward<int&>(lhs)概念的摘要。如果我猜测,我会说Ranges TS决定给出Swappable参数代表表达式,原因如下:

  • 我们可以按照以下几乎给出的Swappable来编写SecondKindOfSwappable

    FirstKindOfSwappable

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

  • 约束template<typename Lhs, typename Rhs = Lhs> concept bool FirstKindOfSwappable = SecondKindOfSwappable<Lhs&, Rhs&>; 预计是一个非常重要的场景;在我的头脑中,它出现在以下业务中:

    swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs))
  • 一致性:在大多数情况下,TS的其他概念遵循相同的约定并且针对表达式的类型进行参数化

但是为什么大部分?

因为存在第三种概念参数:代表...类型的类型。一个很好的例子是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); } ,这个值在通常意义上没有给出有效的表达式(或者使用变量的方法)。

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

  • Arg代表一种类型,即将可构造性作为类型的固有属性
  • Arg是正在构造的变量的声明类型,即约束意味着Arg有效

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

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

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

答案 1 :(得分:4)

我对概念仍然很陌生,所以请随意指出我在这个答案中需要解决的任何错误。答案分为三个部分:第一部分直接关注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:Cubbi's comment,在我测试代码,进行研究和吃晚餐时制作,确认这是一个错字。

[以下内容展开Swappable。如果这与您无关,请随意跳过它。]

请注意,只有在命名空间Swappable之外定义std时,此部分才适用;如果在std中定义,因为它似乎在the draft中,则在重载解析期间会自动考虑两个std::swap(),这意味着不需要额外的工作来包含它们。感谢Cubbi链接到草案并声明Swappable直接来自它。

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

    相反,GCC似乎已达到Catch-22:在推断U之前无法实例化T,但在确定是否为T之前无法推断Swappable Swappable正确解析为U概念......没有U就无法做到这一点。因此,在确定我们是否拥有正确的Swappable之前,需要确定Swappable之前的内容,但是它需要知道我们是否拥有正确的U才能弄明白{{1}}是什么;面对这个无法解决的难题,它有动脉瘤,龙骨,死亡。