简体   繁体   English

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

[英]Why use std::forward in concepts?

I was reading the cppreference page on Constraints and noticed this example: 我正在阅读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));
};

I'm puzzled why they're using std::forward . 我很困惑他们为什么要使用std::forward Some attempt to support reference types in the template parameters? 有些人试图在模板参数中支持引用类型吗? Don't we want to call swap with lvalues, and wouldn't the forward expressions be rvalues when T and U are scalar (non-reference) types? 我们不想用左值调用swap ,并且当TU是标量(非引用)类型时, forward表达式不会是rvalues吗?

For example, I would expect this program to fail given their Swappable implementation: 例如,考虑到他们的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);
}

Unfortunately g++ 7.1.0 gives me an internal compiler error , which doesn't shed much light on this. 不幸的是,g ++ 7.1.0给了我一个内部编译器错误 ,但这并未对此有所了解。

Here both T and U should be MyType , and std::forward<T>(t) should return MyType&& , which can't be passed to my swap function. 这里TU都应该是MyType ,而std::forward<T>(t)应该返回MyType&& ,它不能传递给我的swap函数。

Is this implementation of Swappable wrong? 这个Swappable实现错了吗? Have I missed something? 我错过了什么吗?

Don't we want to call swap with lvalues […] 我们不想用左值调用swap [...]

That's a very good question. 这是一个非常好的问题。 A question of API design specifically: what meaning or meanings should the designer of a concept library give to the parameters of its concepts? API设计的一个问题:概念库的设计者应该赋予其概念参数什么含义或含义?

A quick recap on Swappable requirements. 快速回顾一下可交换的需求。 That is, the actual requirements that already appear in today's Standard and have been here since before concepts-lite: 也就是说,已经出现在今天的标准中的实际要求已经出现在概念之前:

  • An object t is swappable with an object u if and only if: 当且仅当以下情况时,对象t 可与对象u 交换
    • […] the expressions swap(t, u) and swap(u, t) are valid […] [...]表达式swap(t, u)swap(u, t)是有效的[...]

[…] [...]

An rvalue or lvalue t is swappable if and only if t is swappable with any rvalue or lvalue, respectively, of type T . 右值或左值t是可交换的,当且仅当t是可交换与类型的任何右值或左值分别T

(Excerpts butchered from Swappable requirements [swappable.requirements] to cut down on a whole lot of irrelevant details.) (摘录了Swappable要求[swappable.requirements],以减少大量不相关的细节。)

Variables 变量

Did you catch that? 你抓到了吗? The first bit gives requirements that match your expectations. 第一位提供符合您期望的要求。 It's quite straightforward to turn into an actual concept†, too: 变成一个实际的概念†也很简单:

†: as long as we're willing to ignore a ton of details that are outside our scope †:只要我们愿意忽略大量超出我们范围的细节

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

Now, very importantly we should immediately notice that this concept supports reference variables right out of the box: 现在,非常重要的是我们应该立即注意到这个概念支持开箱即用的参考变量:

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

(Now technically the Standard was talking in terms of objects which references aren't. Since references not only refer to objects or functions but are meant to transparently stand for them, we've actually provided a very desirable feature. Practically speaking we are now working in terms of variables , not just objects.) (现在从技术上来说,标准是根据参考文献进行讨论而不是参考文献。由于参考文献不仅涉及对象或功能,而且意味着透明地代表它们,我们实际上提供了一个非常理想的特征。实际上我们现在是在变量方面工作,而不仅仅是对象。)

There's a very important connection here: int&& is the declared type of our variables, as well as the actual argument passed to the concept, which in turn ends up again as the declared type of our lhs and rhs requires parameters. 这里有一个非常重要的连接: int&&是我们变量的声明类型,以及传递给概念的实际参数,而这反过来又会因为我们的lhsrhs的声明类型需要参数而再次结束。 Keep that in mind as we dig deeper. 随着我们深入挖掘,请记住这一点。

Coliru demo Coliru演示

Expressions 表达式

Now what about that second bit that mentions lvalues and rvalues? 那么提到左值和右值的那第二位呢? Well, here we're not dealing in variables any more but instead in terms of expressions . 好吧,这里我们不再处理变量,而是表达式 Can we write a concept for that? 我们能为此写一个概念吗? Well, there's a certain expression-to-type encoding we can use. 好吧,我们可以使用某种表达式到类型的编码。 Namely the one used by decltype as well as std::declval in the other direction. decltype使用的std::declval以及另一个方向的std::declval This leads us to: 这导致我们:

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

Which is what you ran into! 这是你遇到的! And as you found out, the concept must be used in a different way: 正如您所发现的那样,必须以不同的方式使用该概念:

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

If you find that non-obvious, take a look at the connection at play this time: we have an lvalue expression of type int , which becomes encoded as the int& argument to the concept, which gets restored to an expression in our constraint by std::declval<int&>() . 如果你发现非显而易见的话,那么看看这次的连接:我们有一个类型为int的左值表达式,它被编码为概念的int& argument,它被std::declval<int&>()恢复为我们约束中的表达式std::declval<int&>() Or in a more roundabout way, by std::forward<int&>(lhs) . 或者以更迂回的方式,通过std::forward<int&>(lhs)

Coliru demo Coliru演示

Putting it together 把它放在一起

What appears on the cppreference entry is a summary of the Swappable concept specified by the Ranges TS. cppreference条目上显示的内容是Ranges TS指定的Swappable概念的摘要。 If I were to guess, I would say that the Ranges TS settled on giving the Swappable parameters to stand for expressions for the following reasons: 如果我猜测,我会说Ranges TS决定让Swappable参数代表表达式,原因如下:

  • we can write SecondKindOfSwappable in terms of FirstKindOfSwappable as given by the following nearly : 我们可以写SecondKindOfSwappable来讲FirstKindOfSwappable 以下给出:

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

    This recipe can be applied in many but not all cases, making it sometimes possible to express a concept parametrised on types-of-variables in terms of the same concept parametrised on expressions-hidden-in-types. 这个配方可以应用于许多但不是所有情况,使得有时可以根据在表达式隐藏类型中参数化的相同概念来表达在变量类型上参数化的概念。 But it's usually not possible to go the other way around. 但通常不可能采取相反的方式。

  • constraining on swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs)) is expected to be an important enough scenario; 限制swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs))预计是一个非常重要的场景; off the top of my head it comes up in business such as: 在我的头脑中,它出现在以下业务中:

     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); } 
  • consistency: for the most part, other concepts of the TS follow the same convention and are parametrised over types of expressions 一致性:在大多数情况下,TS的其他概念遵循相同的约定,并且针对表达式的类型进行参数化

But why for the most part? 但为什么大部分?

Because there is a third kind of concept parameter: the type that stand for… a type. 因为有第三种概念参数:代表...类型的类型。 A good example of that is DerivedFrom<Derived, Base>() which value does not give you valid expressions (or ways to use variables) in the usual sense. 一个很好的例子是DerivedFrom<Derived, Base>() ,它通常意义上没有给出有效的表达式(或使用变量的方法)。

In fact, in eg Constructible<Arg, Inits...>() the first argument Arg can arguably be interpreted in two ways: 实际上,在例如Constructible<Arg, Inits...>() ,第一个参数Arg可以用两种方式解释:

  • Arg stands for a type, ie taking constructibility as an inherent property of a type Arg代表一种类型,即将可构造性作为一种类型的固有属性
  • Arg is the declared type of a variable being constructed, ie the constraint implies that Arg imaginary_var { std::declval<Inits>()... }; Arg是正在构造的变量的声明类型,即约束意味着Arg imaginary_var { std::declval<Inits>()... }; is valid 已验证

How should I write my own concepts? 我该如何写自己的概念?

I'll conclude with a personal note: I think the reader should not conclude (yet) that they should write their own concepts the same way just because concepts over expressions appear, at least from the perspective of a concept writer, to be a superset of concepts over variables. 最后我将以个人的方式结束:我认为读者不应该(尽管)他们应该以相同的方式编写自己的概念,因为表达式上的概念至少从概念编写者的角度出现,是一个超集变量的概念。

There are other factors at play, and my concern is namely with usability from the perspective of a concept client and all these details I only mentioned in passing, too . 还有其他因素在起作用,我关注的是从概念客户的角度来看可用性以及我刚才提到的所有这些细节 But that doesn't really have to do with the question and this answer is already long enough, so I'll leave that story for another time. 但这并不是真的与这个问题有关,这个答案已经足够长了,所以我会把这个故事留到另一个时间。

I'm still very new to concepts, so feel free to point out any errors I need to fix in this answer. 我对概念仍然很陌生,所以请随意指出我在这个答案中需要解决的任何错误。 The answer is divided into three sections: The first directly regards the use of std::forward , the second expands on Swappable , and the third regards the internal error. 答案分为三个部分:第一部分直接关注std::forward的使用,第二部分扩展了Swappable ,第三部分关注内部错误。

This appears to be a typo 1 , and likely should be requires(T&& t, U&& u) . 这似乎是一个错字1 ,可能应该是requires(T&& t, U&& u) In this case, perfect forwarding is used to ensure that the concept will be properly evaluated for both lvalue and rvalue references, guaranteeing that only lvalue references will be marked as swappable. 在这种情况下,完美转发用于确保对左值和右值引用正确评估该概念,从而保证只有左值引用将被标记为可交换。

The full Ranges TS Swappable concept , which this is based on, is fully defined as: 完整的Ranges TS Swappable概念完全定义为:

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

The concept shown on the Constraints and concepts page is a simplified version of this, which appears to be intended as a minimal implementation of library concept Swappable . 约束和概念页面上显示的概念是这个的简化版本,它似乎是作为库概念Swappable的最小实现。 As the full definition specifies requires(T&&, U&&) , it stands to reason that this simplified version should as well. 由于完整定义指定了requires(T&&, U&&) ,因此这个简化版本也应该是合理的。 std::forward is thus used with the expectation that t and u are forwarding references. 因此使用std::forward ,期望tu是转发引用。

1: Cubbi's comment , made while I was testing code, doing research, and eating supper, confirms that it's a typo. 1: Cubbi在我测试代码,做研究和吃晚餐时发表的评论证实这是一个错字。


[The following expands on Swappable . [以下扩展了Swappable Feel free to skip it if this doesn't concern you.] 如果这与您无关,请随意跳过它。]

Note that this section only applies if Swappable is defined outside namespace std ; 需要注意的是,如果这部分仅适用Swappable被命名空间之外定义std ; if defined in std , as it appears to be in the draft , the two std::swap() s will automatically be considered during overload resolution, meaning no additional work is required to include them. 如果在std定义,因为它似乎在草稿中 ,则在重载解析期间将自动考虑两个std::swap() ,这意味着不需要额外的工作来包含它们。 Thanks go to Cubbi for linking to the draft and stating that Swappable was taken directly from it. 由于去Cubbi链接到的草案,并指出, Swappable直接抽出来。

Note, however, that the simplified form by itself isn't a full implementation of Swappable , unless using std::swap has already been specified. 但请注意,简化形式本身并不是Swappable的完整实现,除非已经指定using std::swap [swappable.requirements/3] states that overload resolution must consider both the two std::swap() templates and any swap() s found by ADL (ie, resolution must proceed as if the using-declaration using std::swap had been specified). [swappable.requirements/3]声明重载解析必须考虑两个std::swap()模板和ADL找到的任何swap() (即,解析必须像using std::swap的using声明一样using std::swap )已指定)。 As concepts cannot contain using-declarations, a more complete Swappable might look something like this: 由于概念不能包含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>;

This expanded Swappable will allow for proper detection of parameters that fulfil the library concept, like so . 这个扩展的Swappable将允许正确检测满足库概念的参数, 如此


[The following regards GCC's internal error, and isn't directly related to Swappable itself. [以下是GCC的内部错误,与Swappable本身没有直接关系。 Feel free to skip it if this doesn't concern you.] 如果这与您无关,请随意跳过它。]

To use this, however, f() needs a few modifications. 但是,要使用它, f()需要一些修改。 Rather than: 而不是:

void f(Swappable& x) {}

One of the following should instead be used: 应改为使用以下其中一项:

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

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

This is due to an interaction between GCC and concept resolution rules, and will probably be sorted out in future versions of the compiler. 这是由于GCC和概念解析规则之间的相互作用,并且可能会在未来版本的编译器中进行整理。 Using a constraint-expression sidesteps the interaction that I believe is responsible for the internal error, making it a viable (if more verbose) stopgap measure for the time being. 使用约束表达式可以回避我认为对内部错误负责的交互,使其成为暂时可行的(如果更详细的)权宜之计。

The internal error appears to be caused by the way GCC handles concept resolution rules. 内部错误似乎是由GCC处理概念解析规则的方式引起的。 When it encounters this function: 遇到此功能时:

void f(Swappable& x) {}

As function concepts can be overloaded, concept resolution is performed when concept names are encountered in certain contexts (such as when used as a constrained type specifier, like Swappable is here). 由于函数概念可以重载,因此在某些上下文中遇到概念名称时会执行概念解析(例如,当用作约束类型说明符时,如此处的Swappable )。 Thus, GCC attempts to resolve Swappable as specified by concept resolution rule #1, in the Concept resolution section of this page : 因此,GCC尝试按照概念解析规则#1的规定, 在本页的Concept resolution部分中解析Swappable

  1. As Swappable is used without a parameter list, it takes a single wildcard as its argument. 由于在没有参数列表的情况下使用Swappable ,因此它将使用单个通配符作为其参数。 This wildcard can match any possible template parameter (whether type, non-type, or template), and thus is a perfect match for T . 此通配符可以匹配任何可能的模板参数(无论是类型,非类型还是模板),因此是T的完美匹配。
  2. As Swappable 's second parameter doesn't correspond to an argument, its default template argument will be used, as specified after the numbered rules; 由于Swappable的第二个参数与参数不对应,因此将使用其默认模板参数,如编号规则后指定的那样; I believe this to be the problem. 我相信这是个问题。 As T is currently (wildcard) , a simplistic approach would be to temporarily instantiate U as either another wildcard or a copy of the first wildcard, and determine whether Swappable<(wildcard), (wildcard)> matches the pattern template<typename T, typename U> (it does); 由于T是当前的(wildcard) ,一种简单的方法是将U临时实例化为另一个通配符或第一个通配符的副本,并确定Swappable<(wildcard), (wildcard)>与模式template<typename T, typename U>匹配template<typename T, typename U> (确实如此); it could then deduce T , and use that to properly determine whether it resolves to the Swappable concept. 它然后可以推导出T ,并使用它来正确地确定它是否解析为Swappable概念。

    Instead, GCC appears to have reached a Catch-22: It can't instantiate U until it deduces T , but it can't deduce T until it determines whether this Swappable correctly resolves to the Swappable concept... which it can't do without U . 相反,GCC似乎已经达到了Catch-22:它不能实例化U直到它推导出T ,但它不能推断T直到它确定这个Swappable是否正确解析为Swappable概念......它不能没有U So, it needs to figure out what U is before it can figure out whether we have the right Swappable , but it needs to know whether we have the right Swappable before it can figure out what U is; 因此,它需要弄清楚U是什么才能确定我们是否拥有正确的Swappable ,但它需要知道我们是否拥有正确的Swappable才能弄清楚U是什么; faced with this unresolvable conundrum, it has an aneurysm, keels over, and dies. 面对这个无法解决的难题,它有动脉瘤,龙骨,死亡。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM