C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Thoughts on the revival of C++0x Concepts

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Mon, 20 Feb 2023 15:41:03 -0500
On Sat, Feb 18, 2023 at 9:46 AM Chen Zhige via Std-Proposals <
std-proposals_at_[hidden]> wrote:
>
> Thoughts on the revival of C++0x Concepts [...]
> Given my limited knowledge of C++, I could hardly give any concrete and
mature solution
> to these problems. But at least I could share some of my thoughts on
these.

You should look into some of the literature (blog posts, WG21 papers) on
why C++20 Concepts were adopted and why C++0x Concepts "failed."
I'd also recommend P0782 specifically; it's not easy to interpret IMHO, but
it demonstrates a real problem with definition checking.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0782r2.html
See also
https://quuxplusone.github.io/blog/2019/07/22/definition-checking-with-if-constexpr/


> The following code provides a concept to describe equality comparable
types:
>
> concept equality_comparable<typename T> {
> bool operator==(const T& x, const T& y);
> bool operator!=(const T& x, const T& y) { return !(x==y); }
> }
>
> When applying the concept EqualityComparable to a template, any use of
the operations which haven't been guaranteed by the concepts will cause a
compilation error.
> So the following code will not compile:
> template <input_iterator Iter, typename Val>
> requires equality_comparable<Iter::value_type, Val>

Well, of course, because your `equality_comparable` concept is a template
of one parameter, not two!
Further down, you give this example:

> concept printable<typename T> {
> void print() { cout << "something cool." << endl; }
> }

It's not clear how the compiler is supposed to determine that `operator==`
in the first example is a non-member, but `print` in the second example is
a member. For the sake of argument, let's pretend that you wrote `friend
bool` instead of `bool` in the first example.

But even then, something simple like

template<class T>
    requires equality_comparable<T>
bool f(T t) {
    return t == t;
}

will fail to compile, because it relies on syntax not guaranteed by the
concept. For example, this `struct Evil` models your `equality_comparable`
concept, but obviously can't be substituted into `f` above.

struct Evil {
    friend bool operator==(const Evil&, const Evil&) { return true; }
    friend void operator==(Evil&, Evil&) {}
};

I also encourage you to try (carefully, conscientiously, paying attention
to syntax) to write down concept definitions in your new style that express
basic STL-style notions like "this type models a bidirectional range."
You'll find it difficult to express "r.begin() must return a bidirectional
iterator" in your base-class-inspired syntax.

> My suggestion is; Considering the risks and the reward, if we could adopt
some of the 0x Concepts
> into the language, don't include the definition checking, we can just
delay checks to instantiation
> like what 20 Concepts do. The definition checking, if needed, could be
added later in some backward-compatible way.

FYI, that is literally what happened in C++20 with "Concepts Lite."
Many people said at the time, "One does not simply 'add definition checking
later'." And they were right.

> Refinement relation
> The 0x Concepts use "refinement" relations to describe the hierarchy of
concepts, such as:
> concept bidirectional_iterator<typename T>
> : forward_iterator<T> { //A bidirectional_iterator is a kind of
forward_iterator
> }
>
> The refinements introduced a serious inflexibility into the 0x Concepts
system because the relations of concepts are typically not hierarchies.

The relations of concepts are always, precisely, hierarchies.
We call this "subsumption," and it is *the* flagship feature that
distinguishes C++20 "concepts" from C++11 "traits."
I would actually have loved C++20 Concepts to use explicit
inheritance-inspired syntax as you show above. Instead, they got a
surprising syntax that looks like simple boolean logical expressions but is
actually hierarchical under the hood, thanks to new semantics assigned to
the `&&` and `||` operators in this specific context.
https://quuxplusone.github.io/blog/2018/11/26/remember-the-ifstream/

template<class T> concept A = true;
template<class T> concept B = true; // B is unrelated to A
template<class T> concept C = A<T> && true; // C subsumes
("inherits-from") A, but not from B

> [...] I think that implicitly generating 20 Concepts from 0x Concepts
could be such a solution. For example, the following code:
>
> concept printable<typename T> {
> void print() { cout << "something cool." << endl; }
> }
>
> [...] is the same as: [...]
>
> template <typename T>
> concept printable_20 = requires(T t) {
> {t.print()} -> std::convertible_to<void>;
> };

You forgot that the former requires `print()` to be noexcept(false), and to
have a return type of exactly `void`, and not to be `static`, and to be a
member function instead of a data member, and... and... See
https://quuxplusone.github.io/blog/2019/09/18/cppcon-whiteboard-puzzle/

> template<printable_20 T>
> void constrained(T const& p) {
> p.print();
> }

Here you have the same old problem: just because you can call `t.print()`
on a `T`, doesn't imply that it's safe to call `t.print()` on a `const T`.

–Arthur

Received on 2023-02-20 20:41:17