Why use std::forward in concepts?

老子叫甜甜 提交于 2019-12-03 04:39:34

Don't we want to call swap with lvalues […]

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?

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:
    • […] the expressions swap(t, u) and swap(u, t) are valid […]

[…]

An rvalue or lvalue t is swappable if and only if t is swappable with any rvalue or lvalue, respectively, of type T.

(Excerpts butchered from Swappable requirements [swappable.requirements] to cut down on a whole lot of irrelevant details.)

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. Keep that in mind as we dig deeper.

Coliru demo

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. 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&>(). Or in a more roundabout way, by std::forward<int&>(lhs).

Coliru demo

Putting it together

What appears on the cppreference entry is a summary of the Swappable concept specified by the Ranges TS. 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:

  • we can write SecondKindOfSwappable in terms of FirstKindOfSwappable as given by the following nearly:

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

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.

In fact, in e.g. Constructible<Arg, Inits...>() the first argument Arg can arguably be interpreted in two ways:

  • Arg stands for a type, i.e. taking constructibility as an inherent property of a type
  • Arg is the declared type of a variable being constructed, i.e. the constraint implies that 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.

This appears to be a typo1, and likely should be 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:

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. As the full definition specifies requires(T&&, U&&), it stands to reason that this simplified version should as well. std::forward is thus used with the expectation that t and u are forwarding references.

1: Cubbi's comment, made while I was testing code, doing research, and eating supper, confirms that it's a typo.


[The following expands on 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; 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. Thanks go to Cubbi for linking to the draft and stating that Swappable was taken directly from it.

Note, however, that the simplified form by itself isn't a full implementation of Swappable, unless using std::swap has already been specified. [swappable.requirements/3] states that overload resolution must consider both the two std::swap() templates and any swap()s found by ADL (i.e., resolution must proceed as if the using-declaration using std::swap had been specified). As concepts cannot contain using-declarations, a more complete Swappable might look something like this:

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.


[The following regards GCC's internal error, and isn't directly related to Swappable itself. Feel free to skip it if this doesn't concern you.]

To use this, however, f() needs a few modifications. 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. 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. 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). Thus, GCC attempts to resolve Swappable as specified by concept resolution rule #1, in the Concept resolution section of this page:

  1. As Swappable is used without a parameter list, it takes a single wildcard as its argument. This wildcard can match any possible template parameter (whether type, non-type, or template), and thus is a perfect match for 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; 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); it could then deduce T, and use that to properly determine whether it resolves to the Swappable concept.

    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. 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; faced with this unresolvable conundrum, it has an aneurysm, keels over, and dies.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!