The last time I used C++ concepts with GCC and the fconcepts flag the following snippet used to work
template <typename T, typename U>
concept equality_comparable = requires(T a, U b) {
{ a == b } -> bool;
{ a != b } -> bool;
};
Apparently this is no longer the case and a return-type-requirement after a compound requirement can now only contain type constraints. If I'm not mistaken this basically means using another concept to satisfy the return-type-requirement.
So the perfectly readable and (for C++ standards) short snippet becomes
template <typename From, typename To>
concept convertible_to = std::is_convertible_v<From, To>;
template <typename T, typename U>
concept equality_comparable = requires(T a, U b) {
{ a == b } -> convertible_to<bool>;
{ a != b } -> convertible_to<bool>;
};
Of course this isn't even a full implementation, but let's ignore that for now. Could someone maybe explain to me why the committee decided to change that? Personally I find that "implicitly used template parameter" in the convertible_to concept extremely irritating and confusing.
The main uses of concepts are: introducing type-checking to template programming. simplified compiler diagnostics for failed template instantiations. selecting function template overloads and class template specializations based on type properties.
A constraint is a requirement that types used as type arguments must satisfy. For example, a constraint might be that the type argument must implement a certain interface or inherit from a specific class. Constraints are optional; not specifying a constraint on a parameter is equivalent to using a Object constraint.
Well, what does this actually mean:
template <typename T, typename U>
concept equality_comparable = requires(T a, U b) {
{ a == b } -> bool;
{ a != b } -> bool;
};
Does it mean a == b
must have type exactly bool
, or does it mean if you decay the type you get bool
(i.e. const bool
or bool&
are ok), or does it mean convertible to bool
(i.e. std::true_type
is ok)? I don't think it's at all clear from the syntax - and any one of these three could be meaningfully desired by a particular concept (as P1452 points out, at the time, the ratio of Same<T>
to ConvertibleTo<T>
in concepts was 40-14).
The paper also goes on to point out that in the Concepts TS, where -> Type
existed, we also had the ability to write something like vector<Concept>
... or -> vector<Concept>
as a requirement. That's a type, but would behave very difficultly with the decltype(())
semantics we adopted in P1084.
Basically I don't think the "perfectly readable" snippet actually is - there are multiple potential meanings for that syntax, all of which can be the desired meaning depending on context. And the most commonly used one at the time (same_as<bool>
) isn't even the one we want here (convertible_to<bool>
).
Personally I find that "implicitly used template parameter" in the convertible_to concept extremely irritating and confusing.
It's novel in C++, but I personally find it reads quite nicely in these cases. Seeing:
{ a == b } -> convertible_to<bool>;
Just reads exactly as the requirement: a == b
needs to be a valid expression that's convertible to bool
. For unary concepts, it makes the usage quite nice since you can use them in place of the somewhat meaningless typename
/class
keyword:
template <range R>
void algo(R&& r);
Which isn't that different from other languages. Like, in Rust for instance:
fn algo<I: Iterator>(i: I)
There the "implicitly used template parameter" is so implicit that it's not even part of the trait declaration, it's implicit there too:
pub trait Iterator { ... }
So even with a longer-form syntax, you'd write where I: Iterator
whereas in C++ you'd still write requires range<R>
.
This isn't strictly related to the original question, but I just find it interesting to add for some other color.
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