Why does Foo({}) invoke Foo(0) instead of Foo()?
Asked Answered
S

1

15

Executables produced by clang 3.5.0 and gcc 4.9.1 from the code

#include <iostream>

struct Foo
{
   Foo() { std::cout << "Foo()" << std::endl; }
   Foo(int x) { std::cout << "Foo(int = " << x << ")" << std::endl; }
   Foo(int x, int y) { std::cout << "Foo(int = " << x << ", int = " << y << ")" << std::endl; }
};

int main()                 // Output
{                          // ---------------------
   auto a = Foo();         // Foo()
   auto b = Foo(1);        // Foo(int = 1)
   auto c = Foo(2, 3);     // Foo(int = 2, int = 3)
   auto d = Foo{};         // Foo()
   auto e = Foo{1};        // Foo(int = 1)
   auto f = Foo{2, 3};     // Foo(int = 2, int = 3)
   auto g = Foo({});       // Foo(int = 0)          <<< Why?
   auto h = Foo({1});      // Foo(int = 1)
   auto i = Foo({2, 3});   // Foo(int = 2, int = 3)
}

behave as commented.

From cppreference: cpp/language/list initialization:

[...]

T( { arg1, arg2, ... } )    (7)

[...]

The effects of list initialization of an object of type T are:

If T is an aggregate type, aggregate initialization is performed.

Otherwise, If the braced-init-list is empty and T is a class type with a default constructor, value-initialization is performed.

[...]

I concluded that Foo({}) should call the default constructor.

Where's the bug?

Shahaptian answered 6/11, 2014 at 20:2 Comment(18)
The bug is in cppreference.Ardyce
Fixed cppreference to read "The effects of list initialization of an object of type T from a non-parenthesized braced-init-list are..." would that have been clearer?Ardyce
@Ardyce I would say no. And would say that T({...}) is not a list initialization of an object of type T, rather than to add vague wording that could be misinterpreted. Cpprefrerence already doesn't claim it to be. But with your proposed fix, it would read that T({...}) is a list initialization of an object of type T from a parenthesized braced init list, which is not the case. It is a list initialization of whatever parameter the chosen constructor has.Pubilis
@Ardyce alternatively it might be worth to show multiple initializer lists, T({...}, {...}, ...) (aswell as in the function case and whereever multiple lists make sense. just as done with arg1, arg2 ...). Then it can't be confused as a list initialization of an object of type T so easily anymore, I think.Pubilis
@JohannesSchaub-litb Quite the contrary, Cppreference did in fact claim that T({args...}) is list-initialization. It was listed (pun intended) on the "list-initialization" page as alternative 7 of the situations in which list-initialization is performed. I think the OP's reading of the page was accurate. The proper fix may be to remove items 5, 6, and 7 from the page altogether.Ardyce
@JohannesSchaub-litb Is this better? The other syntaxes are discussed on the Overload Resolution page. It would seem that whoever made the list initialization page in the first place was enumerating all cases where a braced-init-list can appear in the grammar, regardless of whether those cases actually involve list-initialization.Ardyce
@Ardyce hm, it certainly is list initialization, and it still (IMO) correctly lists it as such. But not list initialization of an object of type T. And indeed the page does not claim that it is. Just as it doesn't claim that alternative 5 initializes a function (perhaps more obvious here).Pubilis
Perhaps it might be a good idea to replace T({...}) by U({...}) to avoid the problem with the text referring to T, which here is not right.Pubilis
@Ardyce BTW I just added a comment on the overload resolution page.Pubilis
@Ardyce and litb, I restored and rearranged the list-initialization bullet points and made the wording more specific about what's getting initialized where.Rabblement
@Cubbi, JohannesSchaub-litb, and Casey Thank you all for clarifying the wording at cppreference.com. Now, referring to "U's constructor's parameter" in case (4) does not seem to be precise since Foo({}) will invoke the default constructor if Foo(int) is removed from the example above.Shahaptian
@cubbi much better. But case 4 is a function call to a constructor, and copy list initializes the parameter. What is direct initialized is the U object by non list initialization.Pubilis
@Shahaptian it still is precise. if the int constructor is removed, you are going to call the copy/move constructor.Pubilis
@litb right, moved (4) into copy-list-init block.Rabblement
@JohannesSchaub-litb What about precision with respect to more than one argument? It seems as if Foo({2, 3}) invokes Foo(int, int).Shahaptian
@JohannesSchaub-litb I see it now, nevermind. :-)Shahaptian
@Rabblement That's much better - thanks for this fix and for all the effort you put into cppreference.Ardyce
@Ardyce btw it would've helped if a link to this SO post accompanied the first edits. It wasn't clear where the confusion originated.Rabblement
F
18

The default constructor is only applicable if you use one single pair of either braces:

auto a = Foo();         // Foo()
auto b = Foo{};         // Foo()

Foo({}) instead will only call constructors with the empty list as the argument, copy-list-initializing the parameter of whatever constructor is chosen. [dcl.init]/16:

If the destination type is a (possibly cv-qualified) class type:
— If the initialization is direct-initialization […] constructors are considered. The applicable constructors are enumerated (13.3.1.3), and the best one is chosen through overload resolution (13.3). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

You have one argument: The empty braced-init-list. There is a list-initialization sequence converting {} to int so the constructor Foo(int) is chosen by overload resolution. The parameter is initialized to zero as {} implies a value-intialization which, for scalars, implies a zero-initialization.

There is no bug in cppreferences documentation either: For (7) it is stated that

7) in a functional cast expression or other direct-initialization, with braced-init-list used as the constructor argument

Which clearly leads to the same result as with the above quote: The constructor is called with the (empty) braced-init-list.

Fossil answered 6/11, 2014 at 20:13 Comment(11)
+1 To clarify for OP, the code is equivalent in this case to auto g = Foo(int{});, and int{} == 0.Bocage
speaking of non-standard approaches, in Visual Studio / msvc, when the "list" contains 1 literal that can be interpreted as an integral type, e.g. {42}, that is not a list - as it should be according to the standard - but it's a "real" integer as far as the compiler - msvc - is concerned; example auto a = {1}, a it's an integer if you use msvc .Alurd
@user2485710: Are you sure that's nonstandard? While I wasn't paying careful attention, I do believe I've read auto a = {1} is supposed to deduce int as the type.Lacroix
@Hurkyl the expected outcome in that particular case is an error because {} doesn't really has a type therefore according to the standard the compiler is supposed to see auto as a request for type deduction, a as a label, = as an assignment/copy ctor, {1} as something without a type with a literal in it; therefore the compiler shouldn't be able to deduce any type for a and that's why it's supposed to generate an error instead of an instance of an integral type . Microsoft added this exception to the rule and I don't think that this exception will make the use of {} better .Alurd
@Alurd Initializer lists don't have a type because they're not expressions. But auto a = {1} is well-formed, and should not produce an error. The type should be deduced as std::initializer_list<int> (it's a much debated special case). If Microsoft deduces int, that's the bug. But you don't have to expect an error.Spinozism
Post-N3922, auto a = {1}; should deduce a std::initializer_list, auto a{1}; should deduce an int, and auto a{1,2}; should be ill-formed.Marimaria
@Marimaria and auto a{(1,2)} will be an int. :pSalvucci
Note: A similar pitfall occurs with the assignment operator; a = {}; will prefer operator=(int) over copy-assigning a temporary Foo()Vitrics
So, if ({}) parameter list is interpreted as a list with a single argument, then the question shifts to the last initialization in the OP example: Foo({2, 3}). According to the OP, it calls the Foo::Foo(int x, int y) constructor. Why?Sharlenesharline
Oh, got it. Conceptually it is interpreted as Foo(Foo{2 , 3}) meaning that copy constructor is the single-argument constructor that wins overload resolution in this case.Sharlenesharline
@AnT The move constructor, actually. You can observe that with -fno-elide-constructors.Fossil

© 2022 - 2024 — McMap. All rights reserved.