In
Effective Modern C++, one of the explanations I have in Item 7 ("Distinguish between
()
and
{}
when creating objects") is this:
If you want to call a std::initializer_list
constructor with an empty std::initializer_list
, you do it by making the empty braces a constructor argument—by putting the empty braces inside the parentheses or braces demarcating what you’re passing:
class Widget {
public:
Widget(); // default ctor
Widget(std::initializer_list<int> il); // std::initializer_list ctor
… // no implicit conversion funcs
};
Widget w1; // calls default ctor
Widget w2{}; // also calls default ctor
Widget w3(); // most vexing parse! declares a function!
Widget w4({}); // calls std::initializer_list ctor with empty list
Widget w5{{}}; // ditto
I recently got a bug report from Calum Laing saying that in his experience, the initializations of
w4
and
w5
aren't equivalent, because while
w4
behaves as my comment indicates, the initialization of
w5
takes place with a
std::initializer_list
with one element, not zero.
A little playing around showed that he was right, but further playing around showed that changing the example in small ways changed its behavior. In my pre-retirement-from-C++ days, that'd have been my cue to dive into the Standard to figure out what behavior was correct and, more importantly, why, but now that I'm supposed to be kicking back on tropical islands and downing piña coladas by the bucket (a scenario that would be more plausible if I laid around on beaches...or drank), I decided to stop my research at the point where things got complicated. "Use the force of the Internet!," I told myself. In that spirit, let me show you what I've got in the hope that you can tell me why I'm getting it. (Maybe it's obvious. I really haven't thought a lot about C++ since the end of last year.)
My experiments showed that one factor affecting whether "
{{}}
" as an argument list yields a zero-length
std::initializer_list<T>
was whether
T
had a default constructor, so I threw together some test code involving three classes, two of which could not be default-constructed. I then used both "
({})
" (note the outer parentheses) and "
{{}}
" as argument lists to a constructor taking a
std::initializer_list
for a template class imaginatively named
X
. When the constructor runs, it displays the number of elements in its
std::initializer_list
parameter.
Here's the code, where the comments in
main
show the results I got under all of gcc, clang, and vc++ at
rextester.com. Only one set of results is shown, because all three compilers produced the same output.
#include <iostream>
#include <initializer_list>
class DefCtor {
public:
DefCtor(){}
};
class DeletedDefCtor {
public:
DeletedDefCtor() = delete;
};
class NoDefCtor {
public:
NoDefCtor(int){}
};
template<typename T>
class X {
public:
X() { std::cout << "Def Ctor\n"; }
X(std::initializer_list<T> il)
{
std::cout << "il.size() = " << il.size() << '\n';
}
};
int main()
{
X<DefCtor> a0({}); // il.size = 0
X<DefCtor> b0{{}}; // il.size = 1
X<DeletedDefCtor> a2({}); // il.size = 0
X<DeletedDefCtor> b2{{}}; // il.size = 1
X<NoDefCtor> a1({}); // il.size = 0
X<NoDefCtor> b1{{}}; // il.size = 0
}
These results raise two questions:
- Why does the argument list syntax "
{{}}
" yield a one-element std::initializer_list
for a type with a default constructor, but a zero-element std::initializer_list
for a type with no default constructor?
- Why does a type with a deleted default constructor behave like a type with a default constructor instead of like a type with no default constructor?
If I change the example to declare
DefCtor
's constructor
explicit
, clang and vc++ produce code that yields a zero-length
std::initializer_list
, regardless of which argument list syntax is used:
class DefCtor {
public:
explicit DefCtor(){} // now explicit
};
...
X<DefCtor> a0({}); // il.size = 0
X<DefCtor> b0{{}}; // il.size = 0 (for clang and vc++)
However, gcc rejects the code:
source_file.cpp:35:19: error: converting to ‘DefCtor’ from initializer list would use explicit constructor ‘DefCtor::DefCtor()’
X<DefCtor> b0{{}};
^
gcc's error message suggests that it may be trying to construct a
DefCtor
from an empty
std::initializer_list
in order to move-construct
the resulting temporary into
b0
. If that's what it's trying to do, and if that's what compilers are supposed to do, the example would become more complicated, because it would mean that what I meant to be a series of single constructor calls may in fact include calls that create temporaries that are then used for move-constructions.
We thus have two new questions:
- Is the code valid if
DefCtor
's constructor is explicit
?
- If so (i.e., if clang and vc++ are correct and gcc is incorrect), why does an
explicit
constructor behave differently from a non-explicit
constructor in this example? The constructor we're dealing with doesn't take any arguments.
The natural next step would be to see what happens when we declare the constructors in
DeletedDefCtor
and/or
NoDefCtor
explicit
, but my guess is that once we understand the answers to questions 1-4, we'll know enough to be able to anticipate (and verify) what would happen. I hereby open the floor to explanations of what's happening such that we can answer the questions I've posed. Please post your explanations in the comments!
---------- UPDATE ----------
As several commenters pointed out, in my code above,
DeletedDefCtor
is an aggregate, which is not what I intended. Here's revised code that eliminates that. With this revised code, all three compilers yield the same behavior, which, as noted in the comment in
main
below, includes failing to compile the initialization for
b2
. (Incidentally, I apologize for the 0-2-1 ordering of the variable names. They were originally in a different order, but I moved them around to make the example clearer, then forgot to rename them, thus rendering the example probably more confusing, sigh.)
#include <iostream>
#include <initializer_list>
class DefCtor {
int x;
public:
DefCtor(){}
};
class DeletedDefCtor {
int x;
public:
DeletedDefCtor() = delete;
};
class NoDefCtor {
int x;
public:
NoDefCtor(int){}
};
template<typename T>
class X {
public:
X() { std::cout << "Def Ctor\n"; }
X(std::initializer_list<T> il)
{
std::cout << "il.size() = " << il.size() << '\n';
}
};
int main()
{
X<DefCtor> a0({}); // il.size = 0
X<DefCtor> b0{{}}; // il.size = 1
X<DeletedDefCtor> a2({}); // il.size = 0
// X<DeletedDefCtor> b2{{}}; // error! attempt to use deleted constructor
X<NoDefCtor> a1({}); // il.size = 0
X<NoDefCtor> b1{{}}; // il.size = 0
}
This revised code renders question 2 moot.
The revised code exhibits the same behavior as the original code when
DefCtor
's constructor is declared
explicit
: gcc rejects the initialization of
b0
, but clang and vc++ accept it and, when the code is run,
il.size()
produces 0 (instead of the 1 that's produced when the constructor is not
explicit
).
---------- RESOLUTION ----------
Francisco Lopes, the first person to post comments on this blog post, described exactly what was happening as regards questions 1 and 2 about the original code I posted. The only thing he didn't do was cite sections of the Standard, which I can hardly fault him for. From my perspective, the key provisions in the C++14 Standard are
- 13.3.1.7 ([over.match.list]), which says that when you have a braced initializer for an object, you first try to treat the entire initializer as an argument to a constructor taking a
std::initializer_list
. If that doesn't yield a valid call, you fall back on viewing the contents of the braced initializer as constructor arguments and perform overload resolution again.
and
- 8.5.4/5 ([dcl.init.list]/5), which says that if you're initializing a
std::initializer_list
from a braced initializer, you copy-initialize each element of the std::initializer_list
from the corresponding element of the braced initializer. The relevance of this part of the Standard was brought to my attention by Marco Alesiani in his comment below.
The behavior of the initializations of
a0
and
b0
, then, can be explained as follows:
X<DefCtor> a0({}); // The arg list uses parens, not braces, so the only ctor argument is
// "{}", which, per 13.3.3.1.5/2 ([over.ics.list]/2) becomes an empty
// std::initializer_list. (Thanks to tcanens at reddit for the
// reference to 13.3.3.1.5.)
X<DefCtor> b0{{}}; // The arg list uses braces, so the ctor argument is "{{}}", which is
// an initializer list with one element, "{}". DefCtor can be
// copy-initialized from "{}", so the ctor's std::initializer_list
// param contains a single default-constructed DefCtor object.
I thus understand the error in
Effective Modern C++ that Calum Laing brought to my attention. The information in the comments (and in
this reddit subthread) regarding how
explicit
constructors affect things is just a bonus.
Thanks to everybody for helping me understand what was going on. All I have to do now is figure out how to use this newfound understanding to fix the problem in the book...