Document Number: P0658R1
Date: 2017-06-11
Audience: Evolution Working Group
Reply-to: Christopher Di Bella christopher@codeplay.com
This document proposes to add alias-declarations to requirement-bodies so that types may be renamed without the need for creating exposition-only concepts that have additional template typenames for convenience and readability.
This document uses diff syntax. Lines that are prefixed with a +
are highlighted
green to indicate a proposed addition to the core language.
Concepts are fun to work with, and fun to design, but anything with more than a few names is tedious to type up, and tedious to read. Consider this rather verbose attempt at drafting a concept for a sequence container:
// using value_type_t, difference_type_t, and concepts from Ranges TS
template <typename X, typename... Args>
concept bool SequenceContaioner = Container<X> && requires {
typename value_type_t<X>;
typename difference_type_t<X>;
requires Constructible<X, difference_type_t<X>, value_type_t<X>>();
requires Construictible<X, initializer_list<value_type_t<X>>>();
requires Assignable<X&, initializer_list<value_type_t<X>>>();
// ...
};
Our current SequenceContainer
is quite jarring to read, so we might like to split
it into SequenceContainer
and detail::SequenceConstructible
:
namespace detail {
template <typename X, typename T, typename D, typename... Args>
concept bool SequenceConstructible = Constructible<X, D, T>() &&
Constructible<X, initializer_list<T>>() &&
Assignable<X&, initializer_list<T>>()
// ...
;
// Other SequenceContainer details here...
} // namespace detail
template <typename X, typename... Args>
concept bool SequenceContainer = Container<X> &&
detail::SequenceConstructible<X, value_type_t<X>, difference_type_t<X>, Args...> // &&
// ...
;
Now we can read the implementation of a SequenceContainer
without the added noise
of value_type_t<X>
, difference_type_t<X>
,
and initializer_list<value_type_t<X>>
at every turn, but we’ve now
created a second, exposition-only concept with exactly one use-case: to facilitate the readability of our usable concept
that (hopefully!) has many use-cases. Furthermore, we need to find a meaningful name for our exposition-only concepts
so that they are easy to understand.
It would be nice to have something that resembles:
template <typename X, typename... Args>
concept bool SequenceContainer = Container<X> && requires {
+ requires using value_type = value_type_t<X>; // type alias value_type, typename requirement for value_type_t<X>
+ requires using difference_type = difference_type_t<X>;
+ using initializer_list_type = initializer_list<value_type>; // just a type alias
requires Constructible<X, difference_type, value_type>();
requires Constructible<X, initializer_list_type>();
requires Assignable<X&, initializer_list_type>();
// ...
};
Exposition-only concepts that exist solely to improve readability are removed.
Let’s now consider an attempt to turn std::inner_product
into a concept-checked
inner_product
:
template <InputIterator I1, Sentinel<I1> S1, InputIterator I2, Sentinel<I2> S2,
CopyConstructible T,
typename Proj1 = identity, typename Proj2 = identity,
IndirectRegularInvocable<projected<I1, Proj1>,
projected<I2, Proj2>> Op2 = multiplies<>,
RegularInvocable<T,
indirect_result_of_t<Op2&(projected<I1, Proj1>,
projected<I2, Proj2>)>> Op1 = plus<>>
requires
Assignable<T&, const T&>() &&
Assignable<T&, result_of_t<Op1&(T,
indirect_result_of_t<Op2&(projected<I1, Proj1>, projected<I2, Proj2>)>)>>()
T inner_product(I1 first1, S1 last1, I2 first2, S2 last2, T init, Op1 op1 = Op1{},
Op2 op2 = Op2{}, Proj1 proj1 = Proj1{}, Proj2 proj2 = Proj2{});
Ugly. There’s lots of repetition, and overall, it’s quite difficult to reason if it’s correct. Let’s tidy that up a bit:
template <InputIterator I1, Sentinel<I1> S1, InputIterator I2, Sentinel<I2> S2,
CopyConstructible T,
typename Op1 = plus<>, typename Op2 = multiplies,
typename Proj1 = identity, typename Proj2 = identity,
typename Arg1 = projected<I1, Proj1>, typename Arg2 = projected<I2, Proj2>,
typename InnerResult = indirect_result_of_t<Op2&(Arg1, Arg2)>,
typename OuterResult = result_of_t<Op1&(T, InnerResult)>>
requires
Assignable<T&, const T&>() &&
IndirectRegularInvocable<Op2, Arg1, Arg2>() &&
RegularInvocable<Op1, T, InnerResult>() &&
Assignable<T&, OuterResult>()
T inner_product(I1 first1, S1 last1, I2 first2, S2 last2, T init, Op1 op1 = Op1{},
Op2 op2 = Op2{}, Proj1 proj1 = Proj1{}, Proj2 proj2 = Proj2{});
That’s much easier on the eyes, and our operations are properly ordered! Sadly, we’ve introduced an opportunity for Crafty Programmer who thinks that they know better than their compiler (and library specification) to manipulate the template specialisation to allow syntactically-compatible-but-semantically-incompatible types to pass the concept check. This is an unlikely scenario, but we can’t rule it out from happening for all concept-checked functions.
It’d be great if we could do this instead:
template <InputIterator I1, Sentinel<I1> S1, InputIterator I2, Sentinel<I2> S2,
CopyConstructible T, typename Op1 = plus<>, typename Op2 = multiplies<>,
typename Proj1 = identity, typename Proj2 = identity>
requires requires {
requires Assignable<T&, const T&>();
+ using Arg1 = projected<I1, Proj1>;
+ using Arg2 = projected<I2, Proj2>;
+ using Inner_result = indirect_result_of_t<Op2&(Arg1, Arg2)>;
+ using Outer_result = result_of_t<Op1&(T, InnerResult)>;
requires IndirectRegularInvocable<Op2, Arg1, Arg2>();
requires RegularInvocable<Op1, T, InnerResult>();
requires Assignable<T&, OuterResult>();
}
T inner_product(I1 first1, S1 last1, I2 first2, S2 last2, T init, Op1 op1 = Op1{},
Op2 op2 = Op2{}, Proj1 proj1 = Proj1{}, Proj2 proj2 = Proj2{});
Unlike the example given in our motivation, this indisputably communicates to both programmers and the compiler that
Arg1
, Arg2
, Inner_result
and
Outer_result
are not supposed to be changed by removing them from a position where
a programmer can accidentally or deliberately provide an alternative type.
This document suggests three compounding proposals, provides minor motivations for each, and then discusses their benefits and drawbacks.
A requirements-body allows alias-declarations.
// Example 1
template <class T>
requires(T t) {
typename value_type_t<T>;
+ using value_type = value_type_t<T>;
{t + t} -> value_type;
}
void foo(T t) {}
// Example 2
template <class T>
requires(T t) {
typename value_type_t<T>;
+ using value_type = value_type_t<T>;
{t + t} -> value_type;
}
void foo(T t)
{
value_type bar{}; // ill-formed, value_type not in scope
}
// Example 3
template <class T>
requires(T t) {
typename value_type_t<T>;
+ using value_type = value_type_t<T>;
{t + t} -> value_type;
}
void foo(T t)
{
using value_type = value_type_t<T>;
value_type bar{}; // okay, value_type in scope
}
The key motivation for this proposal is its simplicity: an alias declared inside a requirement-body stays inside said requirement-body. Names are guaranteed not to leak into other namespaces, including function definitions and class specifications.
However, the author feels that this proposal, while simple, is very short sighted. A programmer that is specifying the requirements for a function or class is probably also writing said code. This proposal may simplify the verbosity of a requirements-body, but the programmer still needs to explicitly type out the type requirement and a second type alias in the function/class definition. Forcing the programmer to write out the same type (with the same name) multiple times is potentially error prone. We explore solutions to these in Proposals 2 and 3.
This proposal is currently being implemented using a branch from GCC trunk.
At present, it is possible to declare an alias inside the requirement-body, but the alias leaks outside of the enclosing body. This means that it isn’t possible to write such code:
template <typename T>
concept bool Foo = requires {
using Bar = double;
};
using Bar = int; // error: Bar already declared on line 3
This is under investigation, and can be found on GitHub.
Proposal 2 complements Proposal 1 by extending aliases declared inside the requirement-body to the function or class definition immediately following the body.
// Example 1
template <class T>
requires(T t) {
typename value_type_t<T>;
using value_type = value_type_t<T>;
{t + t} -> value_type;
}
void foo(T t)
{
value_type bar{}; // ok, value_type in scope
}
// Example 2
template <class T>
concept bool SequenceContainer = Container<X> && requires {
typename value_type_t<X>;
using value_type = value_type_t<X>;
...
};
template <SequenceContainer T>
struct Matrix {
Matrix(initializer_list<value_type> l); // ill-formed: value_type not in scope
};
This is a nice addition to the original proposal, as it allows for names declared inside a requirement-body to be declared only once. Names are still contained outside of the whole function/class scope, so there is no global pollution. This would be great in an associative container environment, for example:
template <class Key, class Value, class Compare, class A>
requires requires {
using key_type = Key;
using mapped_type = Value;
using value_type = pair<key_type, mapped_type>;
requires Allocator<A, value_type>;
}
class map {
// value_type, etc. already declared
};
The author foresees two drawbacks in this proposal:
These drawbacks might not be compatible with the suggestion from Proposal 3 due to verbosity, and the author would like to open a discussion point.
As the first proposal is still being implemented, the technical difficulties of this proposal have not yet been assessed.
The author also believes that this is only half of a complete solution for the problems outlined in the initial proposal: it is still necessary to specify a type-requirement before using the alias-declaration.
Proposal 3 can be either an extension to Proposal 1 or Proposal 2, and allows a type-requirement to be made inline with the alias-declaration.
One might conclude that we can allow alias-declarations to immediately double as a type-requirement, but this doesn’t work, since the following would not be possible:
template <typename T>
requires requires(T t) {
using intializer_type = initializer_list<value_type_t<T>>;
}
void foo(T t);
void bar()
{
foo(vector<int>{}); // ill-formed: vector<int>::initializer_type doesn’t exist
}
The author instead proposes that an alias-declaration be prefixed with the requires
keyword
to indicate it is both an alias, and a requirement; otherwise, it is just an alias. For example,
template <typename T>
requires requires(T t) {
+ requires using value_type = value_type_t<T>;
using initializer_type = initializer_list<value_type>;
requires Constructible<T, initializer_type>();
}
void foo(T t);
void bar()
{
foo(vector<int>{}); // ok: vector<int> satisfies the two requirements
foo(pair<int, int>{}); // ill-formed: pair<int, int>::value_type doesn’t exist
}
In this example, we see that vector
still conforms to the requirements outlined
by foo
: namely, typename value_type_t<T>
,
and Constructible<T, initializer_type>
(for some type initializer_type
).
As stated previously, compatibility between this proposal and the previous proposal may be somewhat difficult.
No assessment has been made regarding technical feasibility at present.
In summary, we have looked at a proposal to allow for alias-declarations in requirement-bodies, a proposal extending the declarations to following functions and classes to reduce duplicate aliasing, and finally, a proposal that combines the aliasing and type-requirement into a single statement to further limit duplicate names.
Although there are some potential issues, the author remains hopeful that they can be ironed out.
Casey Carter and Eric Niebler entertained the initial idea and suggested potential alternative ways to implement this proposal, one of which became the actual implementation.
Tom Honermann and Casey also provided a proof reading of the document, and suggested several improvements.