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!
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With