Monday, November 21, 2016

Help me sort out the meaning of "{}" as a constructor argument

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:
  1. 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?
  2. 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:
  1. Is the code valid if DefCtor's constructor is explicit?
  2. 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...

17 comments:

  1. I'm building an intuition (didn't check standard wording) that outer braces (list-initialization) is favoring list semantics if possible. From this intuition the behavior follows naturally:

    - a0({}) -> () is used to construct with an empty list {}
    - b0{{}} -> outer {} is viewed in list semantics b/c of the initializer_list constructor, so this outer {} is itself tied to the initializer_list, the inner {} initializes an element, and it's all as if it was sugar for b0({{}}). Notice that hence, b0{{}, {}}, etc also works.
    - DeletedDefCtor{} -> this is still valid, meaning, removing default constructor doesn't prevent a0/b0 behaviour.
    - b1{{}} -> This would fail construction if it favored list semantics like in b0{{}}, so the next option is not assume outer {} as tied to the initializer_list argument, but just equivalent to (), meaning that the expression is equivalent to b1({}), since that's what can work.

    ReplyDelete
  2. So, trying to answering the questions just building on the previous intuition:

    1 - Because when there's no default constructor, there's no way for construction to work favoring list semantics, so it fallback to standard construction, like if using parenthesis.

    2 - deleted default construct doesn't prevent from struct initialization. Notice how DeletedDefCtor{} still works. So, this is still used if possible. If you really want not only default construction but also struct initialization you may try adding virtual ~DeletedDefCtor(){} to the DeletedDefCtor class. You'll see that now that b2{{}} won't work anymore. In this case the fallback from list semantics construction to normal construction is not happening.

    3 - Unable to answer this one from basic intuition but you may check that b0{DefCtor{}} is accepted by GCC, so it seems the question boils down to whether {} alone is an explicit enough construction expression, clang seems to think so, GCC doesn't.

    4 - This can be worked out from another "fallback" intuition. Notice how in the explicit case, b0{DefCtor{}} builds a 1-sized list (because that's can be done/interpreted), b0{{}} is a 0-sized list because I'm guessing {} is not an explicit enough construction expression like DefCtor{}, and since it's not, then it's not initializing the element which requires so, so if the inner {} is left to initialize the initializer_list argument, then the outer {} is just not tied to that argument but just direct initialization.

    Sorry if it's crazy enough inference, it should look like built up to explain.

    Sometimes that's what I do to keep moving on: put some sense where there's none :-)

    ReplyDelete
  3. And I guess all of that can be summed up with the initializer_list constructor just taking precedence in overload resolution when list initialization is used, but not eliminating other forms of construction from the resolution.

    ReplyDelete
  4. @Francisco Lopes: I think we may be miscommunicating a bit. In main, every object constructed is constructed using the std::initializer_list constructor in at least some way. ("Def Ctor" is never printed, so we know the default constructor is never called. Whether move or copy constructors are involved is not clear; I'm hoping not.) There's no issue of the std::initializer_list constructor taking precedence in some cases here, because the std::initializer_list constructor is called in every case.

    The classes DefCtor, DeletedDefCtor, and NoDefCtor all lack std::initialzer_list constructors, so for objects of those types, a std::initializer_list constructor is never called.

    In your answer to question 2, you mention struct initialization, by which I assume you mean aggregate initialization. But there are no aggregates (e.g., no structs) in the example. Everything is a class. In those cases where one-element std::initializer_list objects are created, I'm guessing that objects are being default-constructed by interpreting "{}" as an empty parameter list to a T constructor instead of as an empty std::initializer_list<T>, but why?

    ReplyDelete
  5. Ouu, Ouu, I don't have a solution, but I have a name for this: The most curly parse

    ReplyDelete
  6. This is a consequence of the "initializer_list if at all possible" rule. That is to say, the rule is that if {} can be interpreted as initializing a T, it is.

    1. if there is a default constructor, then {} is interpreted as an initializer for T and it calls the initializer-list constructor with an list containing one element. If {} cannot be so interpreted, then the {{}} initialization behaves like ({}).
    2. Because DeletedDefCtor is an aggregate and can be initialized from {} by aggregate initialization. If you give it a private data member it would behave like the "NoDefCtor" case.
    3. I believe that the intent here is in line with GCC's behavior, i.e., ExplicitDefCtor is considered to be constructible from {} for overload resolution purposes, so the compiler goes with the initializer-list interpretation only to reject the program for attempting to use an explicit constructor in copy-list-initialization.

    Explicit default constructors are also subject to a couple recent defect reports, resolved by http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0398r0.html.

    ReplyDelete
  7. For some reason, the compilers identify DeletedDefCtor as an aggregate. You can try, for example:

    class DeletedDefCtor {
    public:
    DeletedDefCtor() = delete;
    int i;
    int j;
    };

    DeletedDefCtor x{0, 1};

    This still compiles on gcc and clang. Regarding the Standard, I think the deleted ctor is user-provided and hence this class should *not* be an aggregate.

    Using {} to initialize an aggregate does not call the default ctor, which is why DeletedDefCtor{} works, and probably also why void foo(DeletedDefCtor); foo({}) works.

    Regarding explicit ctors, please see http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1518
    Also note that explicitness does not affect overload resolution for conversion by brace-initialization, which was supposed to prevent errors. Consider:

    struct Foo
    {
    Foo(short);
    explicit Foo(int) {}
    };

    void foo(Foo) {}

    int main()
    {
    Foo f = 42; // OK, using Foo(short)
    Foo g{42}; // OK, using explicit Foo(int)
    Foo h = {42}; // error: trying to use explicit Foo(int)
    foo({42}); // error: trying to use explicit Foo(int)
    }

    ReplyDelete
  8. I'm not completely sure but here's my attempt (please correct me if I'm wrong):

    - with DefCtor [dcl.init.list]/p5 holds and everything goes smooth
    - with NoDefCtor no temporary can be created and [over.match.list]/p1 kicks in

    Just my guess though.

    ReplyDelete
  9. Aggregate initialization is allowed in the DeletedDefCtor case. http://en.cppreference.com/w/cpp/language/aggregate_initialization

    ReplyDelete
  10. "In your answer to question 2, you mention struct initialization, by which I assume you mean aggregate initialization. But there are no aggregates (e.g., no structs) in the example. Everything is a class."

    Hi Scott, yes I mean that. Using the class keyword won't prevent you from creating a POD, class Foo {public:} and struct Foo {} is essentialy the same thing as far as I know, and aggregate initialization is allowed in all those cases, that's why I said DeletedDefCtor{} still works.

    "There's no issue of the std::initializer_list constructor taking precedence in some cases here, because the std::initializer_list constructor is called in every case."

    Correct, 3rd message about precedence should not be following well my intuition. The "precedence" here regards with interpreting the outer brace as a list expression should take precedence over interpreting it as direct initialization.

    ReplyDelete
  11. In all cases, it's the initializer_list constructor that's used, but if possible that comes by viewing the construction expression as a list expression (elements can be constructed with the inner {}), otherwise, if that fails, the initializer_list constructor is used coupled to direct initialization, and the outer brace becomes like a parenthesis.

    ReplyDelete
  12. Welcome back, Scott!

    The interesting thing here is that DeletedDefCtor is still an aggregate because it doesn't have a user-provided constructor (http://eel.is/c++draft/dcl.fct.def.default#5, you explicitly deleted it but that doesn't count). So DeletedDefCtor{} is still well-formed.

    The different takes on the explicit constructor are resolved by CWG Issue 1518 (https://wg21.link/p0398) - now DefCtor with an explicit constructor is not an aggregate, so trying to initialize it with {} would be ill-formed.

    ReplyDelete
  13. I've updated the post to include revised code that prevents aggregate initialization. Thanks to everybody for pointing out that my original definition of DeletedDefCtor made it an aggregate.

    ReplyDelete
  14. Hi,

    I'm not the author of the answer from the reddit/r/cpp, just posting it for you:

    https://www.reddit.com/r/cpp/comments/5e8oq6/scott_meyers_needs_help/daatihr/

    Thanks

    ReplyDelete
  15. This has bugged me for a long time with linux MAN pages and OSX too.

    I'm more confused after this article.

    ReplyDelete
  16. @T. C., @Marco Alesiani, @Anonymous: Thanks to your comments and to the thread on reddit, I think I now understand why


    X b0{{}};


    yields a one-element std::initializer_list. You can read my reddit post here.

    ReplyDelete
  17. I've now updated the blog post with what I believe to be the resolution to the issues I originally raised. Thanks to everybody for their help.

    ReplyDelete