A plea for a consistent, terse and intuitive declaration syntax

Published Proposal,

This version:
ISO JTC1/SC22/WG21: Programming Language C++


A unified variables and parameters declaration syntax for concepts

1. Introduction

This paper aims to provide a uniform and intuitive syntax to declare function parameters, template parameters, lambda parameters and variables, principally with regard to the concept TS.

In particular, using function template parameters is often needlessly ceremonious, and inconsistencies have started to appear in in the language. For example, lambdas can have inferred auto parameters while functions can’t.

These factors contribute to make C++ harder to teach and encourage juniors developers to mindlessly add token until “it compiles”, regardless of whether the final program is semantically correct.

1.1. Existing declaration syntaxes in C++17

Qualifiers notwithstanding, here is a list of existing way to refer to a type in declarations

Template parameters Function parameters Lambda parameters Variable declaration
template <typename|class T>
template <auto T>
template <literal T>

The concept proposal in its current form ( [P0734R0] would add for template parameters:

template <constraint T>

1.2. Inconsistencies and confusing syntax

As things stands, we notice a number of inconsistencies:

2. Proposed Solution

2.1. Allow auto as a parameter in functions

[P0587r0] argue against allowing auto in function. However, in order to be consistent with lambdas and for the sake of being terse, the declaration of f(auto a) should be valid.

Note that the presence of auto is enough to inform the reader that the function is in fact a function template.

template<...> void f(auto a) (as proposed by [P0587r0]) does not offer any extra information to the user of the function ( nor the compiler ).

Lambda now accepts either a template parameters list, auto parameters or any combination of the two, with great effect. It’s a syntax that developers are already familiar with and it would only be natural for it to be used for functions too, for consistency and simplicity.

There is a wording inconsistency between function template and generic lambda. That may need to be addressed. I would suggest that lambda template would be a more accurate and consistent wording.

2.2. Consistent use of auto and typename

However they are not required in a template parameter declaration. For example, Even T and Even auto T are unambiguous declarations and the latter can be deduced from the former. Therefore, both declaration are well-formed. A set of concepts can be defined or provided by the standard library to further constrain the nature of the non-type non-template template parameters a template accepts. template <Pointer P>, template<Reference R>, template<Integral I> ...

It is important to note that a given concept can not be applied to both a type and a value, so there is never any ambiguity between typename and auto in a template parameter declaration.


2.3. Allow Multiple constraints for each type

A template instantiation must satisfy each and every constrained to be well-formed.

For example:

[p0791r0] further suggests to allow passing concepts as parameters to templates in order to compose various logic constructs, in addition to the existing requires clause. However this is a separate issue that probably needs its own paper, as it is an orthogonal consideration.

I don’t think however that allowing complex concept-based logic constructs in abbreviated function templates or variable declarations would lead to more readable code.

Consider Iterable !ForwardIterable auto f(Movable&&(CopyConstructible||Swappable) foo);

I would argue that this declaration is less readable. The terse, "abbreviated function template" form should handle simple cases. If a type needs to be constrained on complex requirements, then it is certainly better to either introduce a new concept for these specific requirements or to write a proper requires-clause.

2.4. Allow use of concepts consistently and uniformly.

2.4.1. Function parameters

Of course, template functions and lambdas can be constrained.

In both function templates and lambda templates, the keyword auto is required to denote that the callable entity is a template and will need to be instantiated. This is explained more further in : §4.1 As the type of a callable template parameter ( auto means generic ) .

2.4.2. Function return type

The following declaration declare a non-template function with an automatically deduced type which satisfies the Sortable constraint:

A program is ill-formed if the deduced type of the function does not satisfies one of the constraints declared in the return type. For the sake of sanity, specifying different concepts in both the return type and the trailing return type should be ill-formed.

Sortable auto f() -> Iterable auto {}; // ill-formed

2.4.3. Variable declaration

Constraints can be used to refine the type of a variable declaration.

The program is ill-formed if one of the constraint is not satisfied.

As a generalization, and maybe as a mean to generate better diagnostic, I propose that constraints can be applied to non-template or instantiated type. It would serve as a sort of static_assert ensuring that a type keeps the properties the user is expecting. A note on usage of concepts constraints in non-template context.

Iterable vector<int> mays seems odd in first approach, but, it’s a nice way to ensure that library and client code remain compatible, and to easily diagnostic what may have been modified and what why the type’s expected properties changed.

It also provides documentation about what the type is going to be used for.

And most importantly, it allow to generalize the notion of constraint and make it consistent through the language.

Specifying concepts that a concrete type is expected to satisfy is not something that I expect to see often, but it could be useful in code bases that are fast changing.

2.4.4. Dependant names

Constraints could be used to better document the use of a dependant names in definition of a template, instead of in addition to typename

consider the following code

template <typename T>
auto constexpr f(T t) {
    return typename T::Bar(42);

struct S {
    using Bar = unsigned;

constexpr int x = f(S{});

We can replace typename by a constraint.

template <typename T>
auto constexpr f(T t) {
    return Unsigned T::Bar(42);

Of course, typename is a valid optional extra token, meaning the following construct is valid too. Note that we refer to the type T::Bar and not to an instance of T::Bar, so the valid token is typename and not auto.

template <typename T>
auto constexpr f(T t) {
    return Unsigned typename T::Bar(42);

Both these declarations have identical semantics using Foo = Unsigned typename T::Bar using Foo = Unsigned T::Bar

The program is ill-formed if the dependant type does not respect the constraint(s). We can, and probably should, convey the same requirements in a requires-clause. A constraint that cannot be satisfied in the body of a template instantiation causes the program to be ill-formed, SFINAE is not engaged.

2.4.5. type-id alias declarations

Allow the use of Constraints in using directives, in the same manner:

using CopyConstructible Foo = Bar;

template<typename T> using Swappable Foo = Bar<T>;

The program is ill-formed if the constraints can not be satisfied.

Additionally, it may be interesting to constraints parameters of template using-directives

template<Serializable T> using Stream<T> = Iterable<T>;

Here, the Stream type can only be specialized using types that satisfy the Serializable constraint. We use the alias syntax to create a type alias more constrained than the original type. An alias created in this way is neither a specialization nor a new type. Stream is exactly of the type Iterable, aka std::is_same_v<Stream<T>, Iterable<T> == true. However, the alias has additional constraints that are checked upon instantiation. The constraits on the alias are checked before the constraints on the type it is created from.

This system would allow the creation of fine-tuned constrained type alias without having to introduce new types, factories or inheritance.

2.4.6. Don’t allow Constraints everywhere

There is a number of places where allowing concepts in front of a type would either make no sense or be actually harmful.

2.5. Grammar

In the situations illustrated above, the type-id can be preceded by one or more qualified-concept-name The sequence of concept-names and type form the entities to which attributes, and qualifiers are applied.

Swappable Sortable Writable foo = {};
const Swappable Sortable Writable & foo = {};
volatile Swappable Sortable Writable auto* foo = {};
Swappable Sortable Writable auto* const* foo = {};
Swappable Sortable Writable && foo;
extern std::EquallyComparable boost::vector<int> baz;

[p0791r0] gives a good justification for this order, in that constraints are like adjectives applied to a type.

A simpler explanation is that constraints are applied to an unqualified type-id. And qualifiers are applied to a constrained type. Beside the parallel with English grammar rules, putting the concept-names after the type-id would create ambiguities as to whether the constraints are applied on the type or on a value.

auto Sortable v = /*...*/; this would create ambiguity as to whether the type or the value is qualified by "Sortable".

Note that, as specified by [P0734R0] (§17.1), concepts can have template arguments beside the type of which we are checking the constraints.

Container<int> i = { /*...*/ };
Callable<int(int)> f = { /*...*/ };

2.5.1. Summary

3. Multiple Parameters constrained by the same concept

The meaning of void foo(ConceptName auto, ConceptName auto) has been the subject of numerous paper and arguments. I’m strongly in favor of considering the multiple parameters as different types. [p0464r2] exposes the arguments in favor of that semantic better that I ever could.

void foo(ConceptName auto a, decltype(a) b) is probably the best solution in the general case.

The following concept can be added to the stl to allow each types to have different qualifiers. In fact, the Ranges TS provides a similar utility called CommonReference [N4651] 7.3.5

template <typename T, typename U>
concept bool SameType = std::is_same_v<std::decay_t<U>, std::decay_t<T>>;

void f(Concept auto a, SameType<decltype(a)> auto b);

This syntax makes it clear that we want identic types and is, in fact, self documenting.

It is not strictly identic to the non-terse form in regards to how overloads are resolved. If we strictly want the behavior of the non-terse syntax in complex overloads sets, then the non-terse syntaxe should be used. The terse syntax aims at making the simple case simple, not at replacing all existing forms of function template declaration.

Finally, all of these reasons aside, we described how constraints could be used outside of template parameters. In all of these contexts, concepts apply to the type they immediately prefix. If functions parameters were to be treated differently that would create inconsistencies and would ultimately lead to confusion and bugs.

Beside, if the "same type" semantic is used, it won’t be easy, or even possible to express that the function allow different parameter types.

4. On whether auto should be optional.

As described above, in functions ( and lambdas ) parameters templates are introduced by the auto keyword. auto also lets the compiler infer the type of a declaration or a function return type.

When in presence of one or more concepts names, auto is not necessary in that it does not add any semantically significant information for the statement/expression to be parsed correctly and unambiguously.

auto may either signify generic/template parameter or deduced/inferred type depending on the context. The two scenarios should be analysed separately.

4.1. As the type of a callable template parameter ( auto means generic )

People expressed a strong desire to differentiate function templates from regular non-template functions. One of the reasons is that functions and functions templates don’t behave identically.

This argument lead to the abbreviated functions being removed from the current Concept TS [P0696R1].

Given these concerns, it’s reasonable to expect auto not to be optional in the declaration of a function template parameter.

void f(ConceptName auto foo); is a terse enough syntax, that conveys that f is a function template and that foo expect to satisfies a concept.

4.2. As the type of a return type or variable declaration ( auto means inferred )

Consider ConceptName auto foo = /*...*/; and ConceptName foo = /*...*/; Both statements are semantically identical as far as the compiler is concerned.

But, for the reader, they convey slightly different meanings.

In the first case, ConceptName auto foo = /*...*/; , our attention is attracted to the fact that the type of foo is inferred from the RHS expression, and we should probably be mindful of what that expression’s type is.

In the second case ConceptName foo = /*...*/; however, what matters is that foo has the behaviour of ConceptName. Here ConceptName is seen not as a constraint of the type of foo but rather as a polymorphic type which exposes a known set of behaviours. The actual type of foo does not matter. This is why there is no need in that scenario to distinguish concepts from concrete types, as they have the same expected properties.

A developer may wish to express either of these intents and so I think that both constructs should be well-formed, and therefore auto should be optional when declaring a variable or return type.

It also make teaching either as both inference and concepts can be introduced independently of each other. Ultimately, some people may expect auto to be required while some may not. Making it optional makes the language more intuitive for all.

5. Acknowledgments and Thanks

This proposal was inspired by Jakob Riedle’s [p0791r0] proposal. Valuable feedbacks from Michael Caisse, Arthur O’Dwyer, Adi Shavit, Simon Brand.


Informative References

Eric Niebler, Casey Carter. Working Draft, C++ Extensions for Ranges. URL: https://wg21.link/n4651
Tony Van Eerd, Botond Ballo. Revisiting the meaning of "foo(ConceptName,ConceptName)". URL: https://wg21.link/p0464r2
Richard Smith, James Dennett. Concepts TS revisited. 5 February 2017. URL: https://wg21.link/p0587r0
Tom Honermann. Remove abbreviated functions and template-introduction syntax from the Concepts TS. URL: https://wg21.link/p0696r1
Andrew Sutton. Wording Paper, C++ extensions for Concepts. URL: https://wg21.link/p0734r0
Jakob Riedle. Concepts are Adjectives, not Nouns. URL: https://wg21.link/p0791r0
Thomas Köppe. An Adjective Syntax for Concepts. URL: https://wg21.link/p0807r0