Date: 2017-10-12Thomas Köppe <firstname.lastname@example.org>
ISO/IEC JTC1 SC22 WG21 P0807r0
We propose to change the constrained-parameter syntax for concepts to be more general, more explicit and more expressive by using the grammatical structure of “attributive adjective, subject noun, predicate noun”:
|attr. adjective||subj. noun||pred. noun|
This idea is not original; to my best knowledge it was first proposed by Richard Smith in Issaquah 2016, and I have since heard it restated as Richard’s idea by several others, including members of the BSI and on the std-proposals mailing list.
In the present C++ working paper
the “Concepts” feature consists of the core mechanics of template constraints
([temp.constr]), as well as a “convenience syntax” in the form
of constrained-parameters, which offer a short-hand access to a limited subset of
constraints. Specifically, a constrained template like
template <Sortable T>
is syntactic sugar for
template <typename T> requires Sortable<T>
Sortable is a type concept, and more generally the kind of
is deduced from the prototype parameter of the concept.
This syntax is superficially convenient, but it suffers from the problem that it creates
an ambiguity: Does
template <Foo X> have an unconstrained non-type
parameter or a constrained (type or template) parameter? This ambiguity may not be an
immediate concern, but it presents a kind of dead end for the design space: Additional
syntactic shorthands, such as the terse function syntax from the Concepts TS, bring their
own set of new ambiguities. (For example, is
f(Foo x, Foo y) a function or a
function template? Does it have one or two template parameters?)
We propose to improve the situation by revising the constrained-parameter syntax. (The core Concepts engine will remain unaffected.) The proposed changes establish a consistent set of syntactical rules and markers that resolve the present ambiguities and will also work well with future additional syntactic sugar for function and variable declarations.
There are three kinds of entities in C++ that one can parametrise over:
|Kind||Disambiguator||Declaration example||Template parameter example|
|1.||things that have a type||nothing||
|2.||things that are a type||
Templates can be specialised to form all three of these kinds (variable/function templates for (1), class templates for (2), and alias templates for (3)). Let us call the three kinds values, types and templates for short, just for the sake of brevity and for the scope of this proposal (so functions are values for now). When we need to be explicit about the kind of an entity, either because a name is dependent, or because we are specifying template parameters, we distinguish the three kinds with the disambiguator shown in the table above.
If we examine the template parameter grammar in C++ from the perspective of natural language,
an analogy suggests itself: In
... Tmpl, the name of the parameter that is being declared is naturally a predicate
noun, and the word that precedes it is the subject: “The integer is
“A type called
T.”, etc. The disambiguator is a noun making up that
subject, and for the value case, the actual type name of the parameter is the
noun. Where does this leave concept constraints? Constraints constrain something, so they are
a kind of qualification, an attribute. The simplest kind of attribute is the attributive adjective:
A red door, an even number. Constraint names can often be read as adjectives
(or perhaps as some more complex attributive).
This little excursion into natural language suggests a modification to the C++ grammar that makes the language more expressive and that resolves the current problems. The solution is to retain both the attributive adjective and the subject noun in the parameter declaration:
template <Even int N>for
template <int N> requires Even<N>(unary non-type constraint).
template <Sortable typename T>for
template <typename T> requires Sortable<T>(unary type constraint).
template <Rebindable template <typename> typename Tmpl>for
template <typename> typename Tmpl> requires Rebindable<Tmpl>(unary template constraint); but see below.
Both the adjective and the predicate are optional, of course: A parameter can be unconstrained
and need not be named. But the general template parameter now consists of three parts, and there
is now no ambiguity: If
typename is present, it is a type parameter, if
is present, it is a template parameter, and if neither is present, it is a non-type parameter.
A concept that can be used as an adjective in this way is required to be unary, i.e.
to constrain only one template parameter, and it has to have the appropriate
kind of prototype parameter: value concepts must have a non-type parameter of the correct type,
type concepts must have a type parameter, and template concepts must have a template parameter
of a compatible signature (allowing for
... to match non-variadic templates, etc.).
Additional template parameters are allowed, just as they are in the status quo, and are filled in
after the first parameter:
template <VeryEven<A, B, C> int N> becomes
template <int N> requires VeryEven<N, A, B, C>.
For constrained template template parameters, we may wish to use the simpler form
template <Rebindable template Tmpl> and leave the template signature
entirely to be determined by the concept. On the other hand, using the full template
signature allows the user to request a template of a specific signature that is also
constrained by a (possibly more generic) concept. We could also allow both a short form (without
class) that deduces the template signature from the
concept, as well as a long form (with
class). If the
long form is used, the user-provided signature must be compatible with the concept’s
parameter is a non-type parameter whose type is deduced. As such, we may wish to constrain both
its value and also its permissible types.
The most conservative solution is to continue the same logic as above, and require
the concept to have an exactly matching prototype parameter, namely an
template <EvenInteger auto N> becomes
template <auto N>
requires EvenInteger<N>, and the concept could be something like:
However, we can imagine different directions or generalisations:
template <Foo auto N>becomes
template <auto N> requires Foo<decltype(N)>when
Foois a type concept (“automatic
template <Even auto N>might be allowed but a hard error or SFINAE condition when
Nis deduced to anything other than
Let us call a concept whose prototype parameter is
auto an auto
concept. Naturally, auto concepts constrain auto parameters. But we may also allow auto
concepts to constrain typed non-type parameters:
template <EvenInteger long
template <long N> requires EvenInteger<N>
and deduces the type of
long. The adjective syntax allows
us to write specific templates (e.g. using
long) constrained by generic,
reusable concepts (e.g.
A C++14-style generic lambda contains a function template that does not use a template introducer,
and instead declares a parameter with type specifier
stands for a function template of the form
template <typename T> (T x). To allow
constrained generic lambdas, only type constraints may be applied to the (implied) template parameter.
The natural syntax that suggests itself here is to perform “
and admit type-constraining concepts of the form
to stand for
|Constraint kind||Concept example||Example usage|
|Unary non-type template constraint||
template <int N, /*params*/> concept NonTypeFoo = /* ... */;
template <NonTypeFoo</*args*/> int V> struct X; // requires NonTypeFoo<V, /*args*/>
|Unary type template constraint||
template <typename T, /*params*/> concept TypeBar = /* ... */;
template <TypeBar</*args*/> typename T> struct Y; // requires TypeBar<T, /*args*/>
|Unary template template constraint||
template <template <typename, int, typename...> typename Tmpl, /*params*/> concept TemplateQuz = /* ... */;
template <TemplateQuz</*args*/> template < typename, int, bool, char> typename Tmpl> struct Z; // long form // requires TemplateQuz<Tmpl, /*args*/> template <TemplateQuz</*args*/> template Tmpl> struct Z; // short form, Tmpl is <typename, int, typename...> // requires TemplateQuz<Tmpl, /*args*/>
Unary auto template constraints
template <auto N, /*params*/> concept VeryEvenInteger = std::is_integer_v<decltype(N)> && (N % 2 == 0) && OtherReqs</*params*/>;
template <VeryEvenInteger</*args*/> auto N> struct W1; // requires VeryEvenInteger<N, /*args*/> template <VeryEvenInteger</*args*/> std::size_t N> struct W2; // requires VeryEvenInteger<N, /*args*/>, deducing std::size_t
Unary type and non-type constraints
template <typename T, /*params*/> concept VeryIntegral = std::is_integer_v<T> && OtherReqs</*params*/>; template <int N, /*params*/> concept VeryEven = (N % 2 == 0) && OtherReqs</*params*/>;
template <VeryIntegral</*args*/> auto N> struct W3; // requires VeryIntegral<decltype(N), /*args*/> template <VeryEven</*args*/> auto N> struct W4; // requires VeryEven<N, /*args*/>, and decltype(N) must be int
|Function template constraints
on generic lambdas
(type constraints only)
(TypeBar</*args*/> auto x) // equivalent to: <TypeBar</*args*/> typename T>(T x) // equivalent to: <typename T> requires TypeBar<T, /*args*/> (T x) // (hypothetical)
The changes in a nutshell:
autoparameters are decorated with an explicit
autoand can additionally be constrained with type concepts (via “
decltypeunwrapping”) and with value concepts (via required type matching).
auto) that a template is being declared, and that each declared constrained parameter is a distinct template parameter.
We already demonstrated how the adjective syntax may be reused to
allow constrained generic lambdas. Since lambdas share many characteristics with ordinary
functions, it is natural to allow ordinary function templates to use the same parameter
declaration syntax as generic lambdas. Putting the constraint in attributive position
auto keyword as an unmistakable signifier that the declaration is
a function template. (The alternative syntactic shorthand that was present in the Concepts
TS omits the
auto keyword when a constraint is present, which leaves it
unclear at a glance whether a function or a function template is being declared.)
A side note: the syntax in the Concepts TS left it visually unclear whether a repeated constraint
in the parameter list refers to one single or several distinct template parameters, e.g. whether
void f(Foo x, Foo y) is
template <typename T> void foo(T, T) or
template <typename T1, typename T2> void foo(T1, T2). (The TS does have
a definite rule, but the point is that a reader needs to know and remember (or look up)
that rule.) With the proposed adjective style, the function template might be spelled
f(Foo auto x, Foo auto y), which, by analogy with existing uses of
auto in C++17,
makes it reasonably obvious that two distinct template parameters are being declared.
Another kind of type that may be decorated with constraints is the type of a variable or the return
type of a function. At present,
auto is allowed for both; adding a constraint attribute
may conceivably be useful. Moreover, deduction could be allowed for template parameters.
Finally, we might consider allowing more than one concept name to appear before the noun,
interpreted as “and”, as in
template <Sortable Movable typename C>.
The presence of the noun (here
typename) makes it clear that
Movable are concepts.
The obvious alternative to the proposed change is to retain the status quo. The current constrained-parameter syntax is shorter. Type parameters occur much more often than value and template parameters, and so the loss of information about the parameter kind may be an acceptable trade-off. (After all, the purpose of syntactic sugar is to make common constructions convenient.)
We see the value of this proposal only partly in its increased generality and explicitness.
The other part lies in the future directions for other kinds of syntactic shorthands. Reusing
the syntax of the status quo is problematic, since for even shorter kinds of abbreviations
it is prone to ambiguities. By contrast, this proposal offers a simple principle by which
template are consistently
present to signal that a kind of template argument deduction is in place. Additionally, they
offer a natural place for constraints on those arguments.
Another alternative is to place the concept adjective in predicative rather than
attributive position (The door is red. vs. the red door). This would
perhaps require some additional punctuators, e.g.
typename T : Sortable. This
idea seems overly inventive, and even though it is not substantially different from the
proposed attributive position, it would perhaps not play as nicely with future directions
vector<Sortable typename> vs.
Sortable auto x vs.
auto : Sortable x (or even
auto x : Sortable).
Sortable typename T) vs. predicative (
typename T : Sortable)
autoparameters: a) Only
autoconcepts, b) allow
decltype-unwrapping use of type concepts, c) allow value concepts (subject to matching types), d) allow both?
Many thanks to Tom Honermann for valuable feedback.