Document no. | P0915R0 |
Date | 2018-02-08 |
Reply-to | Vittorio Romeo <vittorio.romeo@outlook.com>, John Lakos <jlakos@bloomberg.net> |
Audience | Evolution Working Group |
Project | ISO JTC1/SC22/WG21: Programming Language C++ |
auto
This paper proposes the addition of a concept-constrained auto
placeholder type for variables. The primary goal is to increase code readability and correctness without sacrificing genericity:
template <typename T>
void foo(std::vector<T>& v)
{
auto<RandomAccessIterator> it{std::begin(v)};
// ...
}
A large part of the Concepts TS 1 has been merged into the C++20 Standard working draft 2. Due to the lack of consensus and also to the existence of reasonable concerns, proposed features such as placeholders (changes to [dcl.spec.auto]
) and abbreviated templates (changes to [dcl.fct]
and [temp]
) have not yet been merged into the working draft.
This paper proposes the introduction of a concept-constrained auto
type placeholder, focusing on only the simplest, most common, and most impactful use case, while leaving the door open for future extensions.
The proposed concept-constrained auto
enables users to specify a (constraining) concept-name when declaring variables using auto
:
std::list<int> aList;
// Unconstrained `auto`:
auto i0 = std::begin(aList);
// Well-formed usage of concept-constrained `auto`:
auto<BidirectionalIterator> i1 = std::begin(aList);
// Ill-formed usage of concept-constrained `auto`:
auto<RandomAccessIterator> i2 = std::begin(aList); // <== compile-time error
The current unconstrained auto
placeholder works well in many scenarios:
When the type is obvious from the initialization expression:
auto foo = std::make_shared<Foo>();
std::map<int, std::string> aMap;
auto it = std::begin(aMap);
When the type cannot be spelled out explicitly:
auto lambda = []{ /* ... */; };
Expression templates:
Matrix a{/* ... */}, b{/* ... */}, c{/* ... */};
auto add = a + b;
auto mul = add * c;
consume(mul);
In the snippet above, mul
is not of type Matrix
; its type encodes the sequence of operations as a compile-time expression tree. Using Matrix
instead of auto
could result in performance degradation.
Unfortunately, the inability to constrain auto
with a concept often results in less-readable code, sometimes with surprising results. Consider the following cases:
Invocation of functions in templated contexts:
const auto& myEmployees = getEmployees();
const auto& senior = mostExperiencedOf(myEmployees);
Without reading the signatures (and possibly even the implementations) of getEmployees
and mostExperiencedOf
, it is unclear what kinds of types myEmployees
and senior
might be.
Is
myEmployees
a concrete container? Is it a lazily evaluated range?
Is
senior
a reference to an element insidemyEmployees
? Is it an iterator?
With the proposed concept-constrained auto
, the code becomes more readable and explicit.
const auto<ContiguousContainer>& myEmployees = getEmployees();
auto<RandomAccessIterator> senior = mostExperiencedOf(myEmployees);
More generally, explicit specification of constraining concepts (as in the code snippet above) allows readers to have more refined knowledge about the properties of the types returned by getEmployees
and mostExperiencedOf
. The provided (named) constraints make the operations available on the returned values obvious (easily searchable), without sacrificing genericity or performance. Additionally, failure of the exact types to satify the requirement would be caught early (at initialization, rather than at the point of usage).
Making assumptions about types explicit:
Imagine writing a function that copies the memory of a properly-aligned standard layout class to the GPU. Using (unconstrained) auto
and no static assertions could result in undesirable behavior:
template <typename Producer>
void uploadToGPU(Producer& producer)
{
auto item = producer.next();
gpuMemcpy(dst, &item, sizeof(item)); // <== Potentially UB
}
In the code snippet above, gpuMemcpy
expects &item
to be a pointer to a standard-layout type, but there is nothing enforcing that to be true.
Adding a static_assert
would prevent mistakes from happening, but might also (for some) detract from the readability of the code:
template <typename Producer>
void uploadToGPU(Producer& producer)
{
auto item = producer.next();
static_assert(StandardLayoutType<decltype(item)>);
gpuMemcpy(dst, &item, sizeof(item));
}
Using the proposed concept-constrained auto
would achieve a safe and clear result with minimal boilerplate:
template <typename Producer>
void uploadToGPU(Producer& producer)
{
auto<StandardLayoutType> item = producer.next();
gpuMemcpy(dst, &item, sizeof(item));
}
We are confident that proper use of concept-constrained auto
, where appropriate, will significantly increase the readability and accessibility of Modern C++ programs, especially in large code bases. Note that we are not suggesting that concept-constrained auto
should always be preferred to (unconstrained) auto
-- there are common situations where supplying the optional constraint would be counter indicated:
Employee* Company::findFirstSeniorEmployee() const
{
auto<Iterator> it = std::begin(this->d_employees);
// ^~~~~~~~~~
// ...business logic...
return it == std::end(this->d_employees) ? nullptr
: &*it;
}
In the example above, the identifier it
is already clearly some form of Iterator
. Moreover, the Iterator
concept, along with its idiomatic use as being what is expected of the return types for begin()
and end()
methods, are familiar to virtually every C++ developer. (We refer to such ubiquitously familiar concepts as vocabulary concepts). Making Iterator
an explicit (named) constraint to auto
here would be pointless noise – arguably detracting from readability. If, on the other hand, it were the case that (1) the code did not as yet make explicit use of random-access-iterator features, and (2) it was foreseeable that such features would soon be required, then constraining the auto
with RandomAccessIterator
is exactly what would express that engineering intent:
Employee* Company::findFirstSeniorEmployee() const
{
auto<RandomAccessIterator> it = std::begin(this->d_employees);
// ^~~~~~~~~~~~~~~~~~~~~
// Required to express engineering intent.
// ...business logic that does not require a `RandomAccessIterator` (yet)...
return it == std::end(this->d_employees) ? nullptr
: &*it;
}
From an engineering standpoint, however, the use of concept-constrained auto
becomes imperative, when dealing with non-vocabulary concepts, as this compiler-enforced “documentation” dramatically facilitates developers becoming familiar with previously unseen concepts by name, which – in turn – can be more easily looked up if need be:
void Employee::logStatus() const
{
auto<bdex::OutStream>& stream = getLogStream();
// ^~~~~~~~~~~~~~~~~
stream.putString(this->d_name);
stream.putUint32(this->d_userId);
stream.putUint8(this->d_statusFlags);
}
Developers reading the function above will benefit greatly from the explicit bdex::OutStream
concept constraint, especially those who find it unfamiliar:
Being able to see bdex::OutStream
gives readers a name to search for in the code base and its documentation to understand the scope and functionality of the concept.
The auto<...>
notation (as opposed to the “as yet to be adopted” terse notation) makes it unambiguously clear that bdex::OutStream
is a concept and not a type.
If (unconstained) auto
were used, the reader would need to check the declaration of getLogStream()
(and possibly its definition) in order to understand what kind of object was being returned.
We propose the ability of constraining auto
with a concept, independently of cv-qualifiers. E.g.
auto<Iterator> i0 = foo();
auto<BidirectionalIterator>&& i1 = foo();
const volatile auto<MoveConstructible>& i2 = bar();
The proposed wording copies the terminology used in N4674 in order to make future extensions easier to apply.
[dcl.type.simple]
Add constrained-type-specifier to the grammar for simple-type-specifiers.
Modify paragraph 2 to begin:
auto
specifier is a placeholder for a type to be deduced (10.1.7.4).auto
and constrained-type-specifiers are placeholders for a type to be deduced (10.1.7.4).Add constrained-type-specifier to the table of simple-type-specifiers in Table 11:
Specifier(s) | Type |
---|---|
constrained-type-specifier | placeholder for type to be deduced |
[dcl.spec.auto]
Modify paragraph 1 to begin:
auto
and decltype(auto)
type-specifiers are used to designate a placeholder type that will be replaced later by deduction from an initializer.auto
and decltype(auto)
type-specifiers, and constrained-type-specifier are used to designate a placeholder type that will be replaced later by deduction from an initializer.[dcl.spec.auto.deduct]
Modify paragraph 3 to begin:
auto
type-specifier, the deduced type T'
replacing T
is determined using the rules for template argument deduction.auto
type-specifier or a constrained-type-specifier, the deduced type T'
replacing T
is determined using the rules for template argument deduction.Modify paragraph 3:
U
using the rules of template argument deduction from a function call (17.8.2.1), where P
is a function template parameter type and the corresponding argument is e
. If the deduction fails, the declaration is ill-formed.U
using the rules of template argument deduction from a function call (17.8.2.1), where P
is a function template parameter type and the corresponding argument is e
. If the deduction fails, the declaration is ill-formed. If the used placeholder is a constrained-type-specifier with an associated constraint C
, the declaration is ill-formed if C<U>
evaluates to false
.[dcl.spec.auto.constr]
Add this section to 10.1.7.4.
Paragraph 1:
A constrained-type-specifier designates a placeholder type and introduces an associated constraint.
constrained-type-specifier:
auto
< qualified-concept-name >[Example:
template<typename T> concept bool C0 = true;
int f0() { return 42; }
void f1()
{
auto<C0> i = f0(); // auto<C0> designates a placeholder type
// with associated constraint C0
}
]
This proposal is meant to be as simple as possible but leaves room for future extensions.
Concept-constrained auto
placeholders could be extended to work as function parameter types and also as function return types:
auto l0 = [](auto<ForwardIterator> x){ /* ... */ };
auto foo() -> auto<MoveConstructible> { /* ... */ };
A terser syntax (such as the one already specified in the concepts TS) that doesn’t require auto<...>
to introduce a type placeholder could be incorporated - the concept name on its own would be enough:
ForwardIterator it = foo();
…equivalent to…
auto<ForwardIterator> it = foo();
Note that the proposed syntax is not intended to be a temporary solution until consensus on the terse syntax is reached. We believe that concept-constrained auto
will always be needed even if (when) the terse syntax is voted into the Standard, thanks to its more descriptive and explicit value - especially in public APIs.
Additionally, note that:
ForwardIteratorConcept it = foo();
and
auto<ForwardIterator> it = foo();
have roughly the same number of characters. Yet, ForwardIteratorConcept
is known to be a concept due only to convention, whereas auto<ForwardIterator>
implies that ForwardIterator
is unambiguously a concept. So, regardless of whether or not terse syntax will ever make it into the Standard, the feature we have proposed in this paper is necessary to allow developers to be unambiguously explicit when referring to non-vocabulary concepts.
Logical composition of concepts could be allowed inside the <...>
angle brackets:
auto<Erasable && MoveConstructible> = bar();
The goal of this proposal is to introduce a simplified yet useful minimal subset of the proposed placeholder sections in the Concepts working draft. Concept-constrained auto
’s scope and functionality can then be expanded in the future to reach the power and flexibility of the original design.
<...>
is, in our view, the optimal choice due to its clear, concise, and expressive syntax. Unfortunately, it might not be appropriate as it misleadingly suggests template instantiation. Andrew Sutton recommended using the auto|Concept
syntax instead, which was part of the original proposal for concepts but was dropped in favor of the terse notation which we have already addressed. There exist other viable possibilities for the concept-constrained auto
syntax:
auto{Concept}
auto:Concept
auto Concept
We have not considered auto(Concept)
and auto[Concept]
because they respectively conflict with variable initialization and structured bindings.
“A plea for a consistent, terse and intuitive declaration syntax”:
“Remove abbreviated functions and template-introduction syntax from the Concepts TS”:
“Concepts are Adjectives, not Nouns”
“An Adjective Syntax for Concepts”