简体   繁体   中英

How will concepts lite interact with universal references?

I looked recently at this video explaining the ideas of concepts lite in C++, which are likely to appear this year as a TS. Now, I also learned about universal references/forwarding references (as described here ) and that T&& can have two meanings depending on the context (ie if type deduction is being performed or not). This leads naturally to the question how concepts will interact with universal references?

To make it concrete, in the following example we have

void f(int&& i) {}

int i = 0;
f(i);    // error, looks for f(int&)
f(0);    // fine, calls f(int&&)

and

template <typename T>
void f(T&& test) {}

int i = 0;
f(i);    // fine, calls f(T&&) with T = int& (int& && = int&)
f(0);    // fine, calls f(T&&) with T = int&& (int&& && = int&&)

But what happens if we use concepts?

template <typename T>
    requires Number<T>
void f(T&& test) {}

template <Number T>
void g(T&& test) {}

void h(Number&& test) {}

int i = 0;
f(i);    // probably should be fine?
f(0);    // should be fine anyway
g(i);    // probably also fine?
g(0);    // fine anyway
h(i);    // fine or not?
h(0);    // fine anyway

Especially the last example bothers me a bit, since there are two conflicting principles. First, a concept used in this way is supposed to work just as a type and second, if T is a deduced type, T&& denotes a universal reference instead of an rvalue reference.

Thanks in advance for clarification on this!

It all depends on how the concept itself is written. Concepts-Lite itself ( latest TS as of this writing) is agnostic on the matter: it defines mechanisms by which concepts may be defined and used in the language, but does not add stock concepts to the library.

On the other hand document N4263 Toward a concept-enabled standard library is a declaration of intent by some members of the Standard Committee that suggests the natural step after Concepts-Lite is a separate TS to add concepts to the Standard Library with which to constrain eg algorithms.

That TS may be a bit far down along the road, but we can still take a look at how concepts have been written so far. Most examples I've seen somewhat follow a long tradition where everything revolves around a putative, candidate type that is usually not expected to be a reference type. For instance some of the older Concepts-Lite drafts (eg N3580 ) mention concepts such as Container which have their roots in the SGI STL and survive even today in the Standard Library in the form of 23.2 Container requirements.

A telltale pre-forwarding reference sign is that associated types are described like so:

Value type X::value_type The type of the object stored in a container. The value type must be Assignable, but need not be DefaultConstructible.

If we translate this naïvely to Concepts-Lite, it could look like:

template<typename X>
concept bool Container = requires(X x) {
   typename X::value_type;
   // other requirements...
};

In which case if we write

template<typename C>
    requires Container<C>
void example(C&& c);

then we have the following behavior:

std::vector<int> v;

// fine
// this checks Container<std::vector<int>>, and finds
// std::vector<int>::value_type
example(std::move(v));

// not fine
// this checks Container<std::vector<int>&>, and
// references don't have member types
example(v);

There are several ways to express the value_type requirement which handles this situation gracefully. Eg we could tweak the requirement to be typename std::remove_reference_t<X>::value_type instead.

I believe the Committee members are aware of the situation. Eg Andrew Sutton leaves an insightful comment in a concept library of his that showcases the exact situation. His preferred solution is to leave the concept definition to work on non-reference types, and to remove the reference in the constraint. For our example:

template<typename C>
    // Sutton prefers std::common_type_t<C>,
    // effectively the same as std::decay_t<C>
    requires<Container<std::remove_reference_t<C>>>
void example(C&& c);

T&& always has the same "meaning" -- it is an rvalue reference to T .

The interesting thing happens when T itself is a reference. If T=X&& , then T&& = X&& . If T=X& then T&& = X& . The rule that an rvalue reference to an lvalue reference is an lvalue reference is what allows the forwarding reference technique to exist. This is called reference collapsing 1 .

So as for

template <typename T>
  requires Number<T>
void f(T&& test) {}

this depends on what Number<T> means. If Number<T> permits lvalue references to pass, then that T&& will work like a forwarding reference. If not, T&& it will only bind to rvalues.

As the rest of the examples are (last I checked) defined in terms of the first example, there you have it.

There may be additional "magic" in the concepts specification, but I am not aware of it.


1 There is never actually a reference-to-a-reference. In fact, if you type int y = 3; int& && x = y; int y = 3; int& && x = y; that is an illegal expression: but using U = int&; U&& x = y; using U = int&; U&& x = y; is perfectly legal, as reference collapsing occurs.

An analogy to how const works sometimes helps. If T const x; is const regardless of if T is const . If T_const is const , then T_const x; is also const . And T_const const x; is const as well. The const ness of x is the max of the const ness of the type T and any "local" modifiers.

Similarly, the lvalue-ness of a reference is the max of the lvalue-ness of the T and any "local" modifiers. Imagine if the language had two keywords, ref and lvalue . Replace & with lvalue ref and && with ref . The use of lvalue without ref is illegal under this translation..

T&& means T ref . If T was int lvalue ref , then reference collapsing results in int lvalue ref ref -> int lvalue ref , which translates back as int& . Similarly, T& translates to int lvalue ref lvalue ref -> int lvalue ref , and if T = int&& , then T& translates to int ref lvalue ref -> int lvalue ref -> int& .

This is a difficult thing. Mostly, when we write concepts, we want to focus on the type definition (what can we do with T ) and not its various forms ( const T , T& , T const& , etc). What you generally ask is, "can I declare a variable like this? Can I add these things?". Those questions tend to be valid irrespective of references or cv-qualifications. Except when they aren't.

With forwarding, template argument deduction frequently gives you those over forms (references and cv-qualified types), so you end up asking questions about the wrong types. sigh . What to do?

You either try to define concepts to accommodate those forms, or you try to get to the core type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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