Document Number: P0XXXR0
Date: 2016-xx-xx
Reply-to: tongari95@gmail.com
Audience: EWG

Uniform designated initializers and arguments for C++

1. Introduction

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.

1.1 Terminology

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.

2. Motivation

2.1 Order is ambiguous

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?

2.2 Order is vulnerable

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!

2.3 Don’t specify what you don’t care

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?

2.4 Efficiency and safety

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.

2.5 Select a member of union to initialize

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)?

3. Proposal

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

3.1 Designation syntax

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.

3.2 Designatable entities

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.

3.2.1 Function declaration and designatable parameters

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.

3.2.1.1 Function redeclaration

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.

3.2.1.2 Inheriting constructors

For each inherited constructor, the designatable parameters are also inherited.

3.2.2 Contexts for designated initializers

3.2.3 Contexts for designated arguments

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

3.3 Designator uniqueness

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

3.4 Mapping of designated initializers

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

3.5 Viable functions in overload resolution

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.

3.6 Feature detection

The macro __cpp_designators is defined to indicate the support for this feature in the compiler.

4. Impact on the standard

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.

4.1 Potential enhancement on standard library’s interface

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}

5. Design decisions

5.1 Alternate designation syntax

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.

5.2 Should direct-initialization syntax be supported in designation?

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.

5.3 Should designated base initializer be supported?

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.

5.4 Should template arguments be supported?

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.

5.5 Why not make all function parameters designatable by default?

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.

5.6 Why not allow parameter-name based overloading?

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.

5.7 Alternative of designated arguments

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.

6. Implementation experience

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.

7. Related works