Document Number: P0XXXR0
Date: 2016-xx-xx
Reply-to: tongari95@gmail.com
Audience: EWG
This proposal introduces a uniform syntax for designated initializers in aggregate initialization and designated arguments (a.k.a. named arguments) in function invocation.
In the solution we proposed, designated initializers and arguments, technically are the same thing, the only difference is in what they initialize (aggregate members or function arguments).
The purpose of this proposal is to put the emphasis on the unification rather than the distinction, as we’ll show how the unification is crucial to C++ when it comes to uniform initialization. And we’ll show how this proposal solves an embarrassing overload-resolution problem in the standard sequential containers’ constructors when braced-init-list is used, without changing the constructors’ signatures.
In this proposal, we use the term “designated initializers” and “designated arguments” interchangeably, when “designated arguments” is used, we specifically mean the use in function invocation, otherwise, “designated initializers” is used in general.
Order of members/parameters declaration can be quite arbitrary, sometimes it’s conventional, sometimes it’s crafted for padding purpose, and most of the time it depends on the API designer’s taste and mood. The number of members/parameters could easily become large enough that no programmers would remember their absolute order, even if there are only 2 or 3 parameters, the order could still be quite ambiguous, consider copy(a, b)
, is it copy-from or copy-to?
Suppose you have a function prototype looks like this:
void doSomething(TypeA argA, TypeB argB,
bool optionalFlag1 = false,
bool optionalFlag2 = false);
and at the call side, it’s called without the arguments fully specified:
doSomething(a, b, /*optionalFlag1*/opt1);
Some time later, somebody (not necessarily you) decides to add more functionality to doSomething
, and a non-optional flag is needed so he adds it before the optional ones, like this:
void doSomething(TypeA argA, TypeB argB,
bool requiredFlag,
bool optionalFlag1 = false,
bool optionalFlag2 = false);
Now what happens to the caller above? opt1
no longer refers to optionalFlag1
, but the code still compiles and the behavior is changed silently!
Data members and function parameters can have default values. To initialize an aggregate or invoke a function, you don’t need to specify all the members/arguments, but consider this:
void fn(int a = /*default1*/, int b = /*default2*/);
What if you only want to specify b
and leave a
with the default value? The answer is simple - you can’t, you have to specify a
as well. What’s worse, you don’t always know the default values, consider in generic context:
struct T1
{
void f(int a = 1, int b = 1);
};
struct T2
{
void f(int a = 2, int b = 2);
};
// T should accept T1/T2
template<class T>
void fn(T& obj)
{
obj.f(/*???*/, b)
}
In the generic function fn
, you only want to specify b
, but what’s the default value for a
?
If an aggregate has many members to initialize, in practice, people won’t specify the initializers for each member in aggregate initialization, instead, it’s often done in this way:
struct LongAggregate
{
FieldA a;
FieldB b;
...
FieldN n;
};
LongAggregate agg = {};
agg.a = ...;
agg.b = ...;
...
agg.n = ...;
That means, most of the members are unnecessarily initialized twice, once by value-initialization, another time by later assignment.
And if you don’t initialize agg
at the first place, the uninitialized members may lead to bugs.
Currently there’s no way to initialize the member except the first one in a union
without user-provided constructors.
union U
{
int i;
float f;
};
How could you initialize U
as a float
(f
)?
The above problems can be solved by designated initializers.
To improve readability
copy(.from = a, .to = b);
To avoid silent behavior changes
doSomething(a, b, .optionalFlag1 = opt1);
To ignore what you don’t care
fn(.b = 3);
and for aggregate initialization
LongAggregate agg =
{
.a = ...,
.b = ...,
...
.n = ...
}; // leave unspecified members value-initialized
To select a member of union to initialize
U u = {.f = 3.14};
The proposed syntax is inspired by C99 standard, despite the fact, this proposal does not try to take everything from C99’s designated initializers.
Specifically, these features from C99 are not considered in this proposal:
* array designator, e.g. [0] = 1
* designator list, e.g. .a.b = 1
The syntax .identifier = initializer
is used for designated initializers.
.identifier
is called the designator.
The identifier
refers to a member in an aggregate or an argument in function parameters.
The initializer
is used to initialize the corresponding the member/argument.
Syntactically, designation can only appear in parameter-list or initializer-list.
Designation cannot be followed by ...
, otherwise it’s a syntax error and the program is ill-formed.
There are 2 entities defined to be designatable:
* All the members of an aggregate type.
* Parameters in a function declaration that are explicitly declared as designatable.
In the former, no special syntax is required, because the members are already part of the API.
In the later, a special syntax is required for a parameter to be designatable, as described below.
For a function parameter to be designatable, its name has to be a designator, e.g. int .a
.
Function parameter pack cannot be designatable.
Designatable and non-designatable parameters can be freely mixed in a parameter-list, for example:
void f(int .a, int b, int .c, int d);
where only a
and c
are designatable.
Designatable parameters don’t affect the type of the function.
This feature doesn’t affect how default arguments, function parameter pack and variadic arguments can be specified in a function declaration.
If a function is declared with designatable parameters and/or default arguments, it cannot be redeclared with designatable parameters and/or default arguments, otherwise, the program is ill-formed.
For each inherited constructor, the designatable parameters are also inherited.
Agg agg {.a = 1, .b = 2}
fn(.a = 1, .b = 2)
[](int .a, int .b){}(.a = 1, .b = 2)
Class obj(.a = 1, .b = 2)
Class obj {.a = 1, .b = 2}
new(.a = 1, .b = 2) int
In a function-call expression, if there’s any designated argument and the callee resolves to a pointer-to-function, the call is ill-formed. For example:
void f(int .a);
using FP = void(*)(int);
struct Surrogate
{
operator FP() const { return f; }
};
FP fp = f;
Surrogate s;
f(.a = 1); // ok
(f)(.a = 1); // ok
(&f)(.a = 1); // ill-formed
fp(.a = 1); // ill-formed
s(.a = 1); // ill-formed
Designators in the same aggregate initialization or function invocation should be unique, otherwise it’s a hard error and the program is ill-formed.
For example:
Agg agg = {.a = 1, .a = 2}; // ill-formed
fn(.a = 1, .a = 2); // ill-formed
template<class F, class T, class U>
auto tmp(F f, T t, U u) -> decltype(f(.a = t, .a = u)); // ill-formed
Positional initializers map to the elements (members or parameters) in incremental order, a designator causes the mapping to jump to the matched element, and subsequent mapping begins with the next element in the declaration order.
For template argument deduction, the mapping is done after explicit template arguments are substituted, and every non-deduced parameter pack is ignored.
The mapping is ill-formed if any of the following is true:
* A designator cannot find the corresponding element
* The mapped elements overlap with each other
* The mapped order is out-of-bounds
* An unmapped parameter doesn’t have a default value (designated arguments specific)
The mapping of initializers is performed as if they are reordered in the expression list before evaluation order is considered, this implies:
* In list-initialization, the evaluation order is in members or parameters declaration order
* In a function-call expression, the evaluation order of arguments is unspecified
Given a function template, if the mapping of arguments is ill-formed, the template argument deduction fails.
Given an overload candidate function, if the mapping of arguments is ill-formed, the candidate is not viable.
The macro __cpp_designators
is defined to indicate the support for this feature in the compiler.
The proposed feature only affects parsing, semantic analysis and template instantiation, no codegen or ABI changes are required.
The proposed uniform syntax for designated initializers and arguments inherits the spirit of uniform initialization, it eliminates some unnecessary distinction between aggregates and non-aggregates.
For example:
struct Aggregate
{
int a;
int b;
};
class NonAggregate
{
int a;
int b;
public:
NonAggregate(int .a, int .b) : a(a), b(b) {}
};
Both of Aggregate
and NonAggregate
can be initialized with designated initializers in a similar way:
Aggregate agg = {.a = 1, .b = 2};
NonAggregate non_agg = {.a = 1, .b = 2};
It’s an important abstraction to C++ users - you don’t have to always remember what you are initializing is an aggregate or not, and this is particularly crucial in generic programming.
The proposed feature may also influence the standard library’s interface in future, by making some of the parameters part of the API.
For example, it’s an infamous problem that when braced-init-list is used to initialize a standard sequential container, e.g. std::vector
, the std::initializer_list
constructor overload is preferred even if another constructor overload is a better match:
std::vector<float> v {std::size_t(2), 1.f}; // expected {1.f, 1.f} but got {5.f, 1.f}
Since std::initializer_list
is considered first, there’s no chances for the expected constructor to be selected.
Designatable parameters provide an elegant solution:
vector(size_type .count, // designatable
const T& value,
const Allocator& alloc = Allocator());
explicit vector(size_type .count, // designatable
const Allocator& alloc = Allocator());
This only requires the parameter count
to be part of the API, no changes in the function signature.
With the help of designated arguments, users can use braced-init-list without the fear of ambiguity.
std::vector<float> v {.count = 2, 1.f}; // {1.f, 1.f}
For example, the GNU style id: init
is another candidate. There are reasons why we prefer .id = init
:
* C99 compatible.
* Idiomatic - the .
is the member access operator, making it an intuitive syntax to specify an element in the targeted aggregate/function.
* Code completion - the .
is often used as a trigger point for code completion, helping users to lookup available members/parameters in IDE.
* Language consistency - the =
makes it clear that it’s not direct-initialization.
For example:
f(.a{1}, .b(2));
Agg agg = {.a{1}, .b(2)};
We think the answer should be no. In both aggregate initialization and function invocation, each element is copy-initialized (or reference-initialized), and we’re not going to support direct-initialization in these contexts. It’d be confusing to allow direct-initialization syntax while copy-initialization is actually used.
For example:
struct Derived: Base
{
int a;
};
Derived d = {.Base/*???*/, .a = 1};
We think the answer should be “not yet”. Since C++17, aggregates can now have base classes. But we have no motivating use cases for designated base initializer yet, and since the base initializers will be the leading initializers anyway, we don’t feel a strong requirement for designated base initializers. Besides, as described above, we don’t want to support direct-initialization syntax, so how the initialization syntax for base classes would be is another problem.
For example:
// Not proposed
template<class .T, bool .B>
struct Tmpl {};
Tmpl<.T = int, .B = true> tmp;
We think the answer should be “not yet”. It may come in future proposal if desired.
We think the library author (API designer) should have the right to decide whether he wants to maintain the parameter names as part of the API. While we encourage the use of designatable parameters, we also think it should be an opt-in feature, as many of the parameter names are not designed to be used from outside, especially in the standard library itself, making them designatable by default allows inadvertant use of parameter names which are not intended to be maintained.
For example:
// Not proposed
void f(int .foo);
void f(int .bar);
There are 2 possible semantic for the overloading:
1. Both f
refer to the same function entity.
2. The two f
refer to different function entities.
Path 1 doesn’t affect the type system, but this is not very useful, and probably confusing to the user.
Path 2 needs to interfere with the type system, hence ABI changes (e.g. name mangling) are also required. It’s undesirable that adding designatable parameters to existing API would cause ABI incompatibility, so we don’t think it worth the trouble.
Somebody suggested that designated arguments could be a secondary citizen built upon C99’s designated initializer, the idea is roughly:
* Allow inline aggregate definition in function parameters.
* Syntax sugar for eliminating the braces in function call.
For example:
void f(struct {int a;});
f({.a = 1});
f(.a = 1); // syntax sugar
We don’t think this approach is viable for C++. There are many problems:
* Type definition in function parameters tends to be problematic - if the declaration of f
appear more than once, should the anon-struct be considered the same type?
* No clear way to map the arguments to parameters, where aggregates and normal parameters can be mixed, and braces elision complicates it further.
* Doesn’t play well with overloading and template argument deduction.
For function overloads:
struct I { int a; };
struct F { float a; };
void f(I);
void f(F);
f({1}); // ambiguous
f({.a = 1}); // ambiguous
The call to f
would be ambiguous no matter designated initializers are used or not.
For function templates:
template<class T>
struct A { T a; };
template<class T>
void f(A<T>);
f({1}); // couldn't infer template argument
f({.a = 1}); // couldn't infer template argument
The template argument cannot be deduced no matter designated initializers are used or not.
In contrast, our approach doesn’t suffer from these problems, overloading and template argument deduction are well-supported.
We have implemented the prototype based on Clang, the source code can be reached at:
https://github.com/jamboree/clang/tree/feature/designator
Note that both GCC and Clang already have designated initializers as part of C99 support and C++ extension.