Doc. no.: | P1157R0 |
---|---|
Date: | 2018-07-14 |
Audience: | EWG |
Reply-to: | Zhihao Yuan <zy at miator dot net> |
“[…] the multi-argument concept name introducer notion is the only fully general principled notation.” [1]
constrained upon introduced | unconstrained upon introduced |
---|---|
template<EqualityComparable T> bool operator==(list<T>, list<T>); |
template<class T> requires EqualityComparable<T> bool operator==(list<T>, list<T>); |
template<EqualityComparableWith T U> bool operator==(list<T>, list<U>); |
template<class T, class U> requires EqualityComparableWith<T, U> bool operator==(list<T>, list<U>); |
The motivation of this proposal is to reduce the occurrence of unconstrained type parameters in generic library specifications. Take the following signature for example,
template<class T, class U>
requires EqualityComparableWith<T, U>
bool operator==(list<T> const& x, list<U> const& y);
Here we introduced two type parameters unconstrained to constrain them later. An alternative may introduce one:
template<class T, EqualityComparableWith<T> U>
bool operator==(list<T> const& x, list<U> const& y);
But if the designated concept takes only one argument, with constrained-parameter we don’t have to have any unconstrained parameter:
template<EqualityComparable T>
bool operator==(list<T> const& x, list<T> const& y);
The author believes that it is worth to extend this functionality to apply to the concepts that take more than one arguments:
template<EqualityComparableWith T U>
bool operator==(list<T> const& x, list<U> const& y);
One might consider this to be a minor issue by claiming that multi-parameter type concepts are “rare” or “complex[2].” The rest of this section shows that these are misconceptions.
A survey on the future concepts library[3] and ranges library[4] shows that 47% percent of the concepts specified in the next standard can take multiple arguments:
takes only one argument | may take multiple arguments | |
---|---|---|
Standard Library Concepts | Integral SignedIntegral UnsignedIntegral Swappable Destructible DefaultConstructible MoveConstructible CopyConstructible Boolean EqualityComparable StrictTotallyOrdered Movable Copyable Semiregular Regular |
Same DerivedFrom ConvertibleTo CommonReference Common Assignable SwappableWith Constructible EqualityComparableWith StrictTotallyOrderedWith Invocable RegularInvocable Predicate Relation StrictWeakOrder |
The One Ranges Proposal | Readable WeaklyIncrementable Incrementable Iterator InputIterator OutputIterator ForwardIterator BidirectionalIterator RandomAccessIterator ContiguousIterator Permutable Range SizedRange View CommonRange InputRange ForwardRange BidirectionalRange RandomAccessRange ContiguousRange ViewableRange |
Writable Sentinel SizedSentinel IndirectUnaryInvocable IndirectRegularUnaryInvocable IndirectUnaryPredicate IndirectRelation IndirectStrictWeakOrder IndirectlyMovable IndirectlyMovableStorable IndirectlyCopyable IndirectlyCopyableStorable IndirectlySwappable IndirectlyComparable Mergeable Sortable OutputRange |
One might argue that “can take multiple arguments” does not necessarily mean that this concept wants to declare multiple type parameters at the same time. For example, Sentinel
has only been used for declaring one type parameter in the ranges library:
template<Iterator I, Sentinel<I> S>
void advance(I& i, S bound);
However, Sentinel<S, I>
already requires I
to satisfy Iterator
, so given the proposed functionality, we can rewrite the above generic function as
template<Sentinel S I>
void advance(I& i, S bound);
Although this version introduces the template parameters in the reversed order and has vague meaning, it implies that as long as a concept can take multiple arguments, it can potentially make use of the capability of introducing those template parameters at once. We can trivially resolve both issues mentioned earlier with a new concept that switches the parameters:
template<class I, class S>
concept Traversable = Sentinel<S, I>;
template<Traversable I S>
void advance(I& i, S bound);
It is unclear what does “complex” mean in the term “complex concepts.” It may mean “more stuff to specify,” but there is no evidence showing that the multi-parameter concepts take more LoC to specify comparing to the unary concepts, especially when the Boolean
concept takes 17 LoC. Or it may mean “more logical components,” then one might be surprised that MoveConstructible
is not even a special case of Constructible
,
template<class T>
concept MoveConstructible = Constructible<T, T> && ConvertibleTo<T, T>;
nor does CopyConstructible
:
template<class T>
concept CopyConstructible = MoveConstructible<T> &&
Constructible<T, T&> && ConvertibleTo<T&, T> &&
Constructible<T, const T&> && ConvertibleTo<const T&, T> &&
Constructible<T, const T> && ConvertibleTo<const T, T>;
We must recognize that multi-parameter concepts are not more special comparing to the multi-parameter functions. If they look complicated, the survey on the ranges library shows a possibility – given the status quo, the library can only introduce the template parameters one at a time. Here is an example: we almost always (32 out of 34) use the IndirectUnaryPredicate
concept together with the projected
wrapper,
template<InputIterator I, Sentinel<I> S, class Proj = identity,
IndirectUnaryPredicate<projected<I, Proj>> Pred>
bool all_of(I first, S last, Pred pred, Proj proj = Proj{});
but defining a concept that captures this pattern (see also IndirectlyComparable
)
template<class Proj, class F, class I>
concept ProjectedUnaryPredicate = IndirectUnaryPredicate<F, projected<I, Proj>>;
is not making the specification significantly more clear because we at least need to introduce one type parameter unconstrained:
template<InputIterator I, Sentinel<I> S, class Proj,
ProjectedUnaryPredicate<I, Proj> Pred>
bool all_of(I first, S last, Pred pred, Proj proj);
This verbosity goes away as soon as we can declare multiple template parameters at once:
template<InputIterator I, Sentinel<I> S,
ProjectedUnaryPredicate<I> Proj Pred>
bool all_of(I first, S last, Pred pred, Proj proj);
Combining with
template<class I, class S>
concept InputTraversable = InputIterator<I> && Sentinel<S, I>;
, we can rewrite the full declarations of ranges::all_of
template<InputIterator I, Sentinel<I> S, class Proj = identity,
IndirectUnaryPredicate<projected<I, Proj>> Pred>
bool all_of(I first, S last, Pred pred, Proj proj = Proj{});
template<InputRange Rng, class Proj = identity,
IndirectUnaryPredicate<projected<iterator_t<Rng>, Proj>> Pred>
bool all_of(Rng&& rng, Pred pred, Proj proj = Proj{});
into
template<InputTraversable I S, IndirectUnaryPredicate<I> Pred>
bool all_of(I first, S last, Pred pred);
template<InputTraversable I S, ProjectedUnaryPredicate<I> Proj Pred>
bool all_of(I first, S last, Pred pred, Proj);
template<InputRange Rng, IndirectUnaryPredicate<iterator_t<Rng>> Pred>
bool all_of(Rng&& rng, Pred pred);
template<InputRange Rng, ProjectedUnaryPredicate<iterator_t<Rng>> Proj Pred>
bool all_of(Rng&& rng, Pred pred, Proj);
Now all the template parameters are constrained at the time of being introduced. There is little need for mental type arithmetic after only a few tweaks to the complex specifications that would not benefit from the proposed functionality at first glance, what you see then is what you get.
Because the idea is alien. C++ yet has the following setting: introducing both and is the only way for to declare anything. For example, let be int
, then int a, b;
declares a
and b
, but int
can also introduce only one name with int a;
. Given pair<bool, int> foo();
, auto [a, b] = foo();
declares a
and b
; but auto
can also introduce only one name with auto a = foo();
. The proposed syntax, qualified-concept-name followed by a list of identifiers that has no separator token between them, ensures sufficient unfamiliarity so that nothing else in the language (such as enum-specifier) come to mind when a user see this in code.
About why this specific syntax, continuing our “function” metaphor: a unary function is a multi-parameter function with an arity of 1, so the author comes up with a design such that a unary constrained-parameter, as specified in the working paper[5], not only semantically, but also syntactically, is a multi-argument constrained parameter with an arity of 1.
So we should make template-declaration less ugly. Here is a proposal:
template-head:
template
opt<
template-parameter-list>
requires-clauseopt
explicit-specialization:
template
opt<
>
declaration
With this change, a generic function declaration will get closer to a generic lambda:
auto f = <Iterator I>(I first, I last) { /* ... */ };
<Iterator I> auto g(I first, I last) { /* ... */ }
auto f = <Traversable I S>(I first, S last) { /* ... */ };
<Traversable I S> auto g(I first, S last) { /* ... */ }
It was suggested that a constrained parameter such as Sortable Container X
should declare X
that requires Sortable<X> && Container<X>
. There are two premises for this interpretation to be useful:
Whether it is possible for all Containers
being Sortable
or all Sortable
being Containers
is something we cannot tell from undefined concepts. Therefore the author surveyed the ranges library to look for the signatures that have constraints satisfying 1) and 2).
The survey shows that among all the constraints additionally supplied, one concept (Permutable
) involved is unary; among the 8 algorithms (16 signatures) where Permutable
is spelled out, 3 signatures (iterator version reverse
, stable_partition
, shuffle
) can actually benefit from the adjective conjunction syntax. Note that Permutable
already subsumes ForwardIterator
so sometimes the later is not necessary, but the wording has the convention of not to use an adjective concept-name to declare a constrained-parameter.
So it seems that the suggested idea is not very useful within one generic library. What about the case when the concepts come from different generic libraries? The author does not have real code to make a call but suspects that such a syntax may be misleading because partial ordering independently defined concepts can give unexpected results.
To answer the question in the heading: not worth it.
The short answer is you cannot. A more professional answer is that you can only omit the prototype parameter for a given concept. This rule explains the status quo and uniformly applies to both unary concepts and multi-parameter concepts. For example,
template<class T>
concept EqualityComparable = /* ... */;
template<class T, class U>
concept EqualityComparableWith = /* ... */;
The constrained-parameters EqualityComparable X
, EqualityComparableWith X Y
, and EqualityComparableWith<Y> X
are legal because they are not omitting any name; EqualityComparable
and EqualityComparableWith<Y>
are also legal because they are omitting their prototype parameter T
. But EqualityComparableWith X
is illegal because it intends to omit U
which is not a prototype parameter.
The outcome of the design is WYSIWYG – a reader of the code can easily infer how many template parameters there are from a declaration. If a qualified-concept-name has no identifier to follow it, it introduces an unnamed template parameter; otherwise, the number of the identifiers following it is the number of template parameters it declares.
They are not sufficiently motivated given the examples. It may also be worth noticing that neither Concepts TS[6] nor the in-place syntax[7] allows default arguments on the multi-argument concept name introducer notions.
constrained-parameter:
qualified-concept-name … identifieropt
qualified-concept-name identifieropt default-template-argumentopt
qualified-concept-name identifier-seq
identifier-seq:
identifier
identifier-seq identifierA constrained-parameter introduces a constraint-expression (12.4.2). The expression is derived from the qualified-concept-name Q in the constrained-parameter, its designated concept C, and the declared template parameters P, P, …, P.
An id-expression E is formed as follows. If Q is a concept-name, then E is C<P, P, …, P>. Otherwise, Q is a partial-concept-id of the form C<A, A, …, A>, and E is C<P, P, …, P, A, A, …, A>.
E is the introduced constraint-expression.
This paper proposes only a syntax translation. The capability of this syntax is up to the capability of a unary constrained-parameter. If YAACD[8], or a part of it, is adopted, a multi-argument constrained-parameter will introduce only type parameters; otherwise, given the status quo, a multi-argument constrained-parameter will be able to introduce template parameters of mixed kinds (type, non-type, template):
template<class T, auto N>
concept Array = is_array_v<T[N]>;
template<Array T N>
void foo(std::array<T, N>&);
Stroustrup, Bjarne. P1079 A minimal solution to the concepts syntax problems. http://open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1079r0.pdf ↩︎
Köppe, Thomas. P1142R0 Thoughts on a conservative terse syntax for constraints. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1142r0.html ↩︎
Carter, Casey, and Eric Niebler. P0898R3 Standard Library Concepts. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0898r3.pdf ↩︎
Niebler, Eric, et al. P0896R2 The One Ranges Proposal. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0896r2.pdf ↩︎
Smith, Richard. N4762 Working Draft, Standard for Programming Language C++. https://github.com/cplusplus/draft/raw/master/papers/n4762.pdf ↩︎
Sutton, Andrew. N4674 Working Draft, C++ extensions for Concepts. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4674.pdf ↩︎
Sutter, Herb. P0745R1 Concepts in-place syntax. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0745r1.pdf ↩︎
Voutilainen, Ville, et al. P1141R0 Yet another approach for constrained declarations. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1141r0.html ↩︎