22

我正在阅读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));
};

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

例如,考虑到它们的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 给了我一个内部编译器错误,这并不能说明这一点。

这里Tand Ushould beMyTypestd::forward<T>(t)should returnMyType&&都不能传递给我的swap函数。

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

4

2 回答 2

5

难道我们不想用左值调用交换 […]

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

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

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

[…]

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

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

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&&是我们变量的声明类型,以及传递给概念的实际参数,这反过来又再次成为我们的声明类型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>());
};

你遇到了什么!正如您所发现的,这个概念必须以不同的方式使用:

// 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演示

把它放在一起

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

  • 我们可以用以下几乎SecondKindOfSwappable给出的方式来写:FirstKindOfSwappable

    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>()... };有效

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

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

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

于 2017-06-07T11:38:11.760 回答
4

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

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

基于此的完整范围 TSSwappable概念完全定义为:

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

约束和概念页面上显示的概念是此概念的简化版本,似乎旨在作为库概念的最小实现Swappable。正如完整定义所指定requires(T&&, U&&)的那样,这个简化版本也应该如此是理所当然的。 std::forward因此与期望tu正在转发引用一起使用。

1:Cubbi在我测试代码、做研究和吃晚饭时发表的评论证实这是一个错字。


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

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

但是请注意,Swappable除非using std::swap已经指定,否则简化形式本身并不是 的完整实现。 [swappable.requirements/3]声明重载解析必须同时考虑两个std::swap()模板和swap()ADL 找到的任何 s(即,解析必须像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将允许正确检测满足库概念的参数,就像这样


[以下是 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 尝试Swappable按照概念解析规则 #1 的规定进行解析,在此页面的概念解析部分

  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;面对这个无法解决的难题,它长了一个动脉瘤,倒了下去,死了。

于 2017-06-06T00:37:04.133 回答