Int A[] = {1,2,}; Why Is a Trailing Comma in an Initializer-List Allowed

int a[] = {1,2,}; Why is a trailing comma in an initializer-list allowed?

It makes it easier to generate source code, and also to write code which can be easily extended at a later date. Consider what's required to add an extra entry to:

int a[] = {
1,
2,
3
};

... you have to add the comma to the existing line and add a new line. Compare that with the case where the three already has a comma after it, where you just have to add a line. Likewise if you want to remove a line you can do so without worrying about whether it's the last line or not, and you can reorder lines without fiddling about with commas. Basically it means there's a uniformity in how you treat the lines.

Now think about generating code. Something like (pseudo-code):

output("int a[] = {");
for (int i = 0; i < items.length; i++) {
output("%s, ", items[i]);
}
output("};");

No need to worry about whether the current item you're writing out is the first or the last. Much simpler.

Trailing comma in uniform initialization

First, The C++ grammar rules makes the trailing , optional for braced-init-list. To quote dcl.init/1

A declarator can specify an initial value for the identifier being
declared. The identifier designates a variable being initialized. The
process of initialization described in the remainder of [dcl.init]
applies also to initializations specified by other syntactic contexts,
such as the initialization of function parameters ([expr.call]) or the
initialization of return values ([stmt.return]).

initializer:
brace-or-equal-initializer
( expression-list )
brace-or-equal-initializer:
= initializer-clause
braced-init-list
initializer-clause:
assignment-expression
braced-init-list
braced-init-list:
{ initializer-list ,opt }
{ designated-initializer-list ,opt }
{ }

Secondly, you can't pretty much override the overload resolution system. It will always use the std::initializer_list constructor if you use such syntax and such std::initializer_list constructor is available.

dcl.init.list/2:

A constructor is an initializer-list constructor if its first
parameter is of type std​::​initializer_­list or reference to
possibly cv-qualified std​::​initializer_­list for some type E, and
either there are no other parameters or else all other parameters have
default arguments.
[ Note: Initializer-list constructors are favored over other constructors in list-initialization ([over.match.list])....


The program below prints Using InitList:

#include <iostream>
#include <initializer_list>

struct X{
X(std::initializer_list<double>){ std::cout << "Using InitList\n"; }
X(int){ std::cout << "Using Single Arg ctor\n"; }
};

int main(){
X x{5};
}

Despite the fact that 5 is a literal of type int, it should have made sense to select the single argument constructor since its a perfect match; and the std::initializer_list<double> constructor wants a list of double. However, the rules favour std::initializer_list<double> because its an initializer-list constructor.

As a result, even the program below fails because of narrowing conversion:

#include <iostream>
#include <initializer_list>

struct Y{
Y(std::initializer_list<char>){ std::cout << "Y Using InitList\n"; }
Y(int, int=4){ std::cout << "Y Using Double Arg ctor\n"; }
};

int main(){
Y y1{4777};
Y y2{577,};
Y y3{57,7777};
}

In response to your comment below, "what if there is no overloading with std::initializer_list, or it is not the first constructor's parameter?" - then overload resolution doesn't choose it. Demo:

#include <iostream>
#include <initializer_list>

struct Y{
Y(int, std::initializer_list<double>){ std::cout << "Y Using InitList\n"; }
Y(int, int=4){ std::cout << "Y Using Double Arg ctor\n"; }
};

int main(){
Y y1{4};
Y y2{5,};
Y y3{5,7};
}

Prints:

Y Using Double Arg ctor
Y Using Double Arg ctor
Y Using Double Arg ctor

If there is no initializer-list constructor available, then the {initializer-list...,} initializer pretty much falls back to direct initialization as per dcl.init/16, whose semantics are covered by the proceeding paragraph of dcl.init/16

Why are dangling commas still a thing?

Why weren't they removed?

I don't know if such removal has been considered. I don't know of a reason why such removal would be considered.

To go even further, there would be good reason to add them in case they didn't already exist. They are useful. Indeed, trailing commas have been added in standard revisions into other lists of the languages where they hadn't been allowed before. For example enumerations (C99, C++11). Furthermore, there are proposals to add them to even more lists such as the member init list. I would prefer to see them allowed for example in function calls such as they are allowed in some other languages.

A reason for the allowance of trailing comma is easier modification (less work; less chance of mistake) and clean diffs when programs are modified.

Here are some examples...

Old version without trailing comma:

int array[] = {
1,
2,
3
};

New version:

int array[] = {
1,
2,
3,
4
};

Diff:

<     3
---
> 3,
> 4

Old version with trailing comma:

int array[] = {
1,
2,
3,
};

New version:

int array[] = {
1,
2,
3,
4,
};

Diff:

>     4,

Why is an extra comma not allowed in a parameter list when it is allowed in a brace initialization?

The justification for trailing commas in initializer lists is to allow easy machine-generation of large static arrays. This way, if you happen to need to write a program which generates a C array initializer list, you can just write something like this:

printf("int arr[] = {");
for (int i = 0; i < N; i++) {
printf("%d, ", i);
}
printf("};");

If the trailing comma wasn't permitted, you would have to make sure that it's not generated; and honestly, while it's not hard to do, it's just ugly and a pain in the neck.

There's no practical need to machine-generate large function parameter lists, though, and these lists admittedly look nicer without a trailing comma, so there's no need to permit the same thing in function parameters and calls.

converting to ‘A’ from initializer list would use explicit constructor ‘A::A(int)’

The given program is ill-formed for the reason(s) explained below.

C++20

B is an aggregate. Since you're not explicitly initializing a, dcl.init.aggr#5 applies:


  1. For a non-union aggregate, each element that is not an explicitly initialized element is initialized as follows:

5.2 Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).

This means that a is copy initialized from an empty initializer list. In other word, it is as if we're writing:

A a = {}; // not valid see reason below

Note also that list-initialization in a copy initialization context is called copy-list-initialization which is the case here. And from over.match.list#1.2:

In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.


Essentially, the reason for the failure is that A a = {}; is ill-formed.



C++11

Since B is an aggregate and there are fewer initializer-clauses in the list than there are members in the aggregate, and from aggregate initialization documentation:

The effects of aggregate initialization are:

  • If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default member initializers, if provided in the class definition, and otherwise (since C++14) copy-initialized from empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates).

This again means that a is copy initialized from an empty list {}. That is, it is as if we're writing:

A a = {}; // not valid see reason below

But from over.match.funcs:

In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

So again we face the same problem i.e., A a = {}; is not valid.



Solution

To solve this, we can pass A{} or A{0} as the initializer inside the list as shown below:

B b = { A{} };  //ok now 
B c = { A{0} }; //also ok

Working demo.



Note

Note that writing A a{}; on the other hand is well-formed as this is a direct-initialization context and so it is direct-list-initialization.

Overload resolution of int vs std::vector<int> with an initializer list of a single int

{0} doesn't have a type, so we need to try and convert it to the parameter types of the overload set. When considering

void foo([[maybe_unused]] const std::vector<int>& v) {}

We need to consult [over.ics.list]/7.2 which states

Otherwise, the implicit conversion sequence is a user-defined conversion sequence whose second standard conversion sequence is an identity conversion.

so we have a user defined conversion for this conversion sequence.

Looking at

void foo([[maybe_unused]] int i) {}

We find the conversion covered in [over.ics.list]/10.1 which states

if the initializer list has one element that is not itself an initializer list, the implicit conversion sequence is the one required to convert the element to the parameter type;

The element in this case is 0, which is an integer literal which is an exact match standard conversion

So now we have a user defined conversion vs a standard conversion and that is covered by [over.ics.rank]/2.1

a standard conversion sequence is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and

and now we know that the standard conversion is a better conversion and that is why the int overload is chosen over the std::vector<int> overload.

C++ Overwrite initializer list in unit test

expensiveObject cannot be assigned null. What you might want is to have a smart pointer to ExpensiveObject, and have multiple constructors or better you want to inject your dependencies.

class Example
{
private:
std::shared_ptr<ExpensiveObject> expensiveObject;

public:
Example(std::shared_ptr<ExpensiveObject> ptr) : expensiveObject(ptr) {
//... constructor code
}

methodA() {
//... some code
}
}

Now you can test it for null scenarios as well

Example ex{nullptr};
ex.methodA();

Trailing commas and C++

C++03 (which is a fairly minor update of C++98) bases its C compatibility on C89 (also known as C90, depending on whether you're ANSI or ISO). C89 doesn't allow the trailing comma. C99 does allow it. C++11 does allow it (7.2/1 has the grammar for an enum declaration).

In fact C++ isn't entirely backward-compatible even with C89, although this is the kind of thing that if had it been in C89, you'd expect C++ to permit it.

The key advantage to me of the trailing comma is when you write this:

enum Channel {
RED,
GREEN,
BLUE,
};

and then later change it to this:

enum Channel {
RED,
GREEN,
BLUE,
ALPHA,
};

It's nice that only one line is changed when you diff the versions. To get the same effect when there's no trailing comma allowed, you could write:

enum Channel {
RED
,GREEN
,BLUE
};

But (a) that's crazy talk, and (b) it doesn't help in the (admittedly rare) case that you want to add the new value at the beginning.



Related Topics



Leave a reply



Submit