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:
- 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
What Is the Partial Ordering Procedure in Template Deduction
Do Rvalue References to Const Have Any Use
C/C++: Force Bit Field Order and Alignment
C++ Pass an Array by Reference
Remove_If Equivalent For Std::Map
Registering a Cpp Dll into Com After Installation Using Wix Msi Installer
"Downcasting" Unique_Ptr≪Base≫ to Unique_Ptr≪Derived≫
Why Can't Variable Names Start With Numbers
Why Can't I Compile an Unordered_Map With a Pair as Key
C++ Array - Expression Must Have a Constant Value
Create N-Element Constexpr Array in C++11
Why Will Std::Sort Crash If the Comparison Function Is Not as Operator ≪