Thoughts on Concepts, P0745, and P1079
I just read through Bjarne Stroustrup’s paper P1079R0 “A minimal solution to the concepts syntax problems”. This paper is a response to Herb Sutter’s P0745 “Concepts in-place syntax”. To understand P1079 you have to understand P0745, and to understand P0745 you have to understand what came before (which is to say, the current state of Concepts).
Current concepts syntax
In the current working draft, the syntax for defining classically polymorphic algorithms versus generic algorithms looks like this:
class BorkableBase {
virtual int bork() const = 0;
};
// classically polymorphic function
void make_bork(const BorkableBase& x) {
x.bork();
}
template<class T>
concept Borkable = requires(const T& t) {
{ t.bork() } -> int;
};
template<class T> // unconstrained template
void make_bork(const T& x) {
x.bork();
}
Notice that for the unconstrained template, we have essentially just one syntax to learn. But for constrained templates, the working draft of C++2a permits at least these three different syntaxes, each of which has a slightly different SFINAE effect:
template<class T> // constrained template, option 1
requires Borkable<T>
void make_bork(const T& x) {
x.bork();
}
template<class T> // constrained template, option 2
void make_bork(const T& x) requires Borkable<T> {
x.bork();
}
template<Borkable T> // constrained template, option 3
void make_bork(const T& x) {
x.bork();
}
Terse syntax
The Concepts TS proposed two additional syntaxes, on top of these three, which did not (yet) make it into the working paper. These are the so-called “terse” syntaxes:
Borkable{T}
void make_bork(const T& x) {
x.bork();
}
void make_bork(const Borkable& x) {
x.bork();
}
Notice that both of these definitions still define function templates,
not functions; but neither one uses the keyword template
anywhere.
Also notice that the second and tersest variation specifically looks
exactly like the original, classically polymorphic, non-template
make_bork
; the insignificant difference being that we wrote
const Borkable& x
(indicating a template) instead of const BorkableBase& x
(indicating a non-template).
This is why “terse syntax” was left out of the working paper. People really didn’t like the idea of functions turning into templates merely because of the spelling of a user-defined identifier. People (including myself) would like some hint when looking at code whether we’re looking at a template or not. It doesn’t matter so much when you’re looking at well-written, bug-free code; but it seems like it would matter a great deal when you’re looking at unfamiliar code trying to track down a bug.
Compare the situation we had in C++03 with
class Widget : public Gadget, public Digit {
std::map<std::string, int> fields_;
public:
int get_value(const std::string& name) const {
return fields.at(name);
}
};
Is get_value
a virtual method, or not? We don’t know from its definition;
we have to look at the class definitions of Gadget
and Digit
to find out if
either of them have a virtual int get_value(const std::string&) const
member
function. (And what if one of them has virtual int get_value(const char *) const
,
or virtual int get_value(const std::string&)
, or virtual int getvalue(const std::string&) const
?
Did we just find a typo-bug in Widget
, or is its behavior intentional?)
That problem got solved by adding unambiguous syntax for “I want to be virtual
”,
namely, override
. We can get the compiler to diagnose any (accidentally) virtual
method which lacks override
, and the compiler is required to diagnose any
(accidentally) non-virtual method which is specified as override
. Adding
special-purpose syntax has rescued us from a source of confusing bugs in unfamiliar
code.
P0745 in-place syntax
Herb’s P0745 proposes a compromise syntax that’s still terse-ish, but varies just enough from the syntax of a classically polymorphic function. He proposes both of these new syntaxes instead of the terse syntaxes from the TS:
template<Borkable{T}>
void make_bork(const T& x) {
x.bork();
}
void make_bork(const Borkable{T}& x) {
x.bork();
}
The first syntax doesn’t make any sense to me, because I cannot understand how the compiler is supposed to distinguish between this set of template-introducers, all of which define a template non-type parameter…
template<int x> // C++98
template<int> // C++98
template<auto x> // C++17
template<auto> // C++17
template<auto{T} x> // P0745
template<auto{T}> // P0745
template<Borkable{T} x> // P0745
template<Borkable{T}> // P0745
…and this set, all of which define a template type parameter.
template<class>
template<class T>
template<Borkable T> // WP but not P0745
template<Borkable{T}> // P0745
So we’ll leave the first syntax alone and focus on the second syntax.
void make_bork(const Borkable{T}& x) {
x.bork();
}
This syntax makes a great deal of sense to me, although I stop well short of
saying that I want it in C++2a. (Honestly I don’t think I even want
“constrained template, option 3” to remain in C++2a. I would be very happy if there were
a precise 1:1 relationship between constraints in my codebase and requires
-clauses
in my codebase.)
P1079’s objections and observations
Part of the ongoing problem, which P0745 acknowledges but does not really solve, is that there is another kind-of ambiguity in the working paper’s third syntax.
template<XXX> concept Borkable = true;
template<Borkable B> void foo();
int main() {
foo<int>(); // A
foo<1>(); // B
}
When compiled with -DXXX=class
, line A compiles and line B doesn’t.
When compiled with -DXXX=auto
, line B compiles and line A doesn’t.
So, in order to know what a given template declaration means, we have to know the kinds of template parameters
taken by each of its associated concepts. This is not an ambiguity from the compiler’s point of view
(since it knows everything), but it is a potential source of confusion for the human reader.
(Sidenote: It is an ambiguity for GCC’s current -fconcepts
,
based on the Concepts TS,
which supports “function-style concepts” which are overloadable just like any other function template.
Fortunately, function-style concepts did not make it into C++2a.)
In P1079R0 “A minimal solution to the concepts syntax problems”, Bjarne Stroustrup writes:
I suspect the real problem is that we would like values of types that match the concepts we otherwise use. That is, we might need pairs of concepts, such as
Arithmetic
andArithmetic_value
.template<typename T> concept Arithmetic = requires { /* ... */ }; template<auto N> concept Arithmetic_value = Arithmetic<decltype(N)>;
We can now write:
template<Arithmetic N, Arithmetic_value n> void f(N); // type concept and value concept
I consider this simple and elegant, and it works today. […] Incidentally, this technique mirrors the
_v
and_t
naming convention for type traits. That is, we already use naming conventions to help people where compilers have no problems.
Except for the ugly use of giraffe case,
I’m personally willing to accept this argument at this point. It would be even better if we could
agree that concepts apply only to types, and there is no use-case for a “value concept,” and we
should just remove them from the working paper — but if we can’t agree on that, I hope we can
at least agree to suffix all of our value concepts with Value
.
// Acceptable IMHO: Suffix value concepts with `Value`.
template<int I> concept NonnegativeValue = (I >= 0);
template<class T, NonnegativeValue N>
class my_array { ... };
// Preferable IMHO: Don't use value concepts.
template<class T, int N> requires (N >= 0)
class my_array { ... };
P1079 also raises an interesting objection to P0745’s Iterator{It}
:
template<Number{N}> void f(N); // N is a type name
This is “odd”… We don’t usually use clustering syntax for a single element.
Here the phrase “clustering syntax” seems to refer to the curly braces, which indeed are typically used in C++ to indicate sequences of things: array initializers, or constructor parameters, or the sequence of statements making up a compound statement.
I wonder if this phrase “clustering syntax” indicates growing mainstream acceptance of the idea that curly-brace-initializers
should be used primarily for sequences (such as vector<int>{1,2,3}
) and avoided when the
constructor parameters are not a sequence (such as vector<int>{1,2,Alloc{}}
).
It is, arguably, “odd” that the 99-percent-most-common case in P0745 syntax is for the {}
to enclose only
a single element and not a sequence. However, notice that P0745’s syntax cleanly allows for
multi-argument concepts without the programmer needing to learn any additional syntax:
template<SwappableWith{T,U}>
void swap_with(T& t, U& u) {
using std::swap;
swap(t, u);
}
To rewrite this swap_with
so that it tersely expresses the actually appropriate
constraint — requires(SwappableWith<T&, U&>)
— is left as an exercise for the reader,
as is to ponder the interaction between concepts and cv-ref-qualification in general.