auto
?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”:
template < |
Sortable |
typename |
T |
> void f(T& collection); |
template < |
Even |
int |
N |
> void g(Foo (&pairs)[N]); |
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
(N4687),
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>
when Sortable
is a type concept, and more generally the kind of T
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 | int a = x; (x is a value) |
template <int N> |
2. | things that are a type | typename |
using T = typename foo::x; (x is a type) |
template <typename T> |
3. | templates | template |
using A = typename foo::template x<T> (x is a template) |
template <template </* ... */> typename Tmpl> |
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 int N
, typename T
, template
... 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 N
.”,
“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 template
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
typename
and class
) that deduces the template signature from the
concept, as well as a long form (with typename
or class
). If the
long form is used, the user-provided signature must be compatible with the concept’s
prototype parameter.
auto
?An auto
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 auto
parameter.
For example, 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:
auto
parameters: template <Foo auto N>
becomes template <auto N> requires Foo<decltype(N)>
when Foo
is a
type concept (“automatic decltype
unwrapping”).template <Even auto N>
might be allowed but a hard error or SFINAE condition
when N
is deduced to anything other than int
.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
N>
becomes template <long N> requires EvenInteger<N>
and deduces the type of N
as long
. The adjective syntax allows
us to write specific templates (e.g. using long
) constrained by generic,
reusable concepts (e.g. EvenInteger
).
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 auto
, where (auto x)
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 “decltype
unwrapping”
and admit type-constraining concepts of the form
to stand for
params/args
examples
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 on auto parameters
|
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:
auto
parameters are decorated with an
explicit auto
and can additionally be constrained with type concepts (via
“decltype
unwrapping”) 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
leaves the 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 void
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 Sortable
and
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
the keywords auto
, typename
, and 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
(e.g. vector<Sortable typename>
vs. vector<typename :
Sortable>
), or Sortable auto x
vs.
auto : Sortable x
(or even auto x : Sortable
).
Sortable typename T
) vs. predicative (typename T : Sortable
)auto
parameters: a) Only auto
concepts, 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.