C++ concepts compound requirements and return-type

2020-07-13 10:47发布

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.

标签: c++ c++20
1条回答
贼婆χ
2楼-- · 2020-07-13 11:38

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.

查看更多
登录 后发表回答