Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why use std::forward in concepts?

Tags:

I was reading the cppreference page on Constraints and noticed this example:

// 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. 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?

For example, I would expect this program to fail given their Swappable implementation:

#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.

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.

Is this implementation of Swappable wrong? Have I missed something?

like image 206
aschepler Avatar asked Jun 04 '17 17:06

aschepler


People also ask

What is the point of std :: forward?

std::forwardReturns an rvalue reference to arg if arg is not an lvalue reference. If arg is an lvalue reference, the function returns arg without modifying its type.

Why do we need perfect forwarding?

Perfect forwarding reduces excessive copying and simplifies code by reducing the need to write overloads to handle lvalues and rvalues separately.

What do std :: move and std :: forward do?

std::move takes an object and casts it as an rvalue reference, which indicates that resources can be "stolen" from this object. std::forward has a single use-case: to cast a templated function parameter of type forwarding reference ( T&& ) to the value category ( lvalue or rvalue ) the caller used to pass it.

What is a forwarding reference C++?

When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.

Why is std forward used twice in an idiomatic way?

Here is an example where std::forward is used twice in an idiomatic way: Here is a possible implementations for std::forward. First of all std::forward is more complex than std::move. This version is the result of several iterations.

Why can't I call std forward without the std::forward function?

Removing the std::forward would print out requires lvalue and adding the std::forward prints out requires rvalue. The func is overloaded based on whether it is an rvalue or an lvalue. Calling it without the std::forward calls the incorrect overload. The std::forward is required in this case as pass is called with an rvalue.

What does std::forward actually do?

There are a number of good posts on what std::forward does and how it works (such as here and here ). In a nutshell, it preserves the value category of its argument.

What is the use of forward in C++?

The idiomatic use of std::forward is inside a templated function with an argument declared as a forwarding reference, where the argument is now lvalue, used to retrieve the original value category, that it was called with, and pass it on further down the call chain (perfect forwarding).


1 Answers

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.

like image 111
Luc Danton Avatar answered Nov 11 '22 03:11

Luc Danton