难道我们不想用左值调用交换 […]
这是一个非常好的问题。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&&
是我们变量的声明类型,以及传递给概念的实际参数,这反过来又再次成为我们的声明类型lhs
和rhs
需要参数。当我们深入挖掘时,请记住这一点。
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 条目上的是Swappable
Ranges 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>()... };
有效
我应该如何编写自己的概念?
我将以个人笔记结束:我认为读者不应该(还)认为他们应该以相同的方式编写自己的概念,因为至少从概念作者的角度来看,概念优于表达式,是一个超集变量的概念。
还有其他因素在起作用,我关心的是从概念客户的角度来看的可用性以及我只是顺便提到的所有这些细节。但这实际上与问题无关,而且这个答案已经足够长了,所以我将把这个故事留到另一个时间。