How will concepts lite interact with universal references?
Asked Answered
N

3

15

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 (i.e. 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!

Naturalism answered 21/3, 2015 at 11:39 Comment(1)
In your second example, with f(0), T would be int, not int&&Firedog
C
6

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 e.g. 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 (e.g. 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. E.g. 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. E.g. 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);
Conjunctiva answered 21/3, 2015 at 13:52 Comment(4)
"(latest TS as of this writing)" What's with N4377?Desultory
@Desultory I think it’s one of those ISO things where two documents cover the same changes. One is the TS and the other is the Proposed Draft, whatever that is.Conjunctiva
So this means, that you would have to make sure for every custom concept that it is not a reference, if you want to prevent unexpected forwarding behavior, right? Wouldn't it then be way simpler if a concept ensures this by default? Then a user of the concept always would have to explicitely remove the reference, if he wants to use the forwarding syntax.Naturalism
@Naturalism Sometimes a reference type is a perfectly fine model of a concept. Sometimes a reference type is not a match and you really want the requirements to reject them. Your suggestion does not handle those two situations.Conjunctiva
E
3

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

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; that is an illegal expression: but 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 constness of x is the max of the constness 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&.

Estrous answered 21/3, 2015 at 13:32 Comment(3)
N4377 from 2015-02-09 still defines h like g and g like f: From the abbreviated function template, introduce a template parameter list with a constrained-parameter [dcl.fct]p16-18, then introduce a type template-parameter from the prototype parameter of the concept [temp.param]p9, and finally introduce a predicate constraint from the concept [temp.param]p10.Desultory
No such thing as an rvalue reference to an lvalue reference, or in fact a reference to a reference in general as per §8.3.2/5 "There shall be no references to references […]". There is such a thing as reference collapsing, but I believe there are better ways to explain it.Conjunctiva
@LucDanton long discussion of reference collapsing not using standardese added. Do you see any errors?Estrous
R
3

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.

Rabbit answered 8/9, 2015 at 2:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.