Aaron McDaid
Aaron McDaid

Reputation: 27133

How to control when initializer_list is used. Local vars, returning, and passing args

When writing code, I'm usually very clear in my own mind about whether or not I want to call a constructor that has a single initializer_list constructor. But I often don't feel confident when writing code that I can specify what will happen. There appear to be changes planned for c++17, around auto, which simply add to my paranoia. I don't want to understand all the corners cases to understand everybody else's code. I just hope to fix some simple rules for writing code.

Ideally, an answer would have two parts: "If you want the init-list constructor, do X1,Y1,Z1. But if you don't want it, then do X2,Y2,Z2. If you read code that doesn't clearly follow one of these patterns exactly, then things get complicated and you could consider rewriting "

Pre-question: Is there a convenient name for constructors that have exactly one argument, which is of type std::initializer_list<T>? This constructor appears to be 'special', so it should have a clear name.

Are there any simple rules-of-thumb to ensure that the desired behaviour is achieved? Perhaps something like:

  • (This is just a hypothetical list, probably incorrect)

  • a return followed by a { will always call the init-list constructor (if present)

  • in a variable definition, = followed by { will also prefer the init-list constructor

  • foo(3, ???, true) will construct the second arg of foo as an init-list if ??? takes this form (beginning with { I guess)

And of course, I also would like to be able to do the opposite consistently. If returning a vector<int> for example, how do I return in such a way that I get the non-init-list constructors? (And also avoid the most-vexing-parse, of course!).

I can see three scenarios where this is relevant. Initializing variables in a function, returning from a function, and passing args to a function. And I guess there are other interesting places too.

If we can identify a few non-ambiguous patterns that cover all these use-cases, which behave the same in c++11/14/17, then I would simply avoid any code that doesn't match that pattern and replace it with something that is clear to me.

Upvotes: 0

Views: 85

Answers (2)

Aaron McDaid
Aaron McDaid

Reputation: 27133

TLDR: Embrace the ( parentheses ).

There are two sides to this question. First, if an initializer-list constructor exists and we want to call it, what should we write? Second, how to we avoid such a constructor and call some other constructor that exists? A famous example of the second case is vector, which has a two-arg constructor vector::vector(size_t, T init) which seems difficult to call in the new regime.


1. Calling the the init-list constructor consistently:

Use ({ and }). This will call the init-list constructor, where it exists. It can even pass an empty list, so there's no "empty list exception" whereby the default constructor is called instead. Even though { is known to be quite "greedy", calling the init-list constructor quite readily, it will (surprisingly?) prefer to call the default constructor if one is available. The greediest (concise) form that I am aware of is ({:

X x({});      // passes an empty init-list
X x({1});     // passes a one-element init-list
X x({1,2});   // passes a two-element init-list
auto x = X({});      // as above
auto x = X({1});     // as above
auto x = X({1,2});   // as above

(I must admit that, in the particular case of an empty list, I guess the default constructor probably is a perfectly fine choice. I can't imagine a real situation where I'd really prefer to force the init-list constructor to be used with an empty list.)

2. Avoiding the init-list constructor

We can't use { because they are greedy, so just use (. If you're scared of the most-vexing-parse (you should be!), then just ensure you always use this in an auto declaration

auto x = X( ) ; //  default constructor
auto x = X( 1 ); // one-arg non-init-list constructor
auto x = X(1,2); // two-arg non-init-list constructor
auto v = std::vector<int>(5,0);   // five elements, not two. As desired

Basically, to solve the most vexing parse, use auto instead of switching ( to {.



Finally, you also asked about return. This doesn't appear to be a straightforward issue. Reliably selecting a constructor to be used requires putting the type explicitly in the return statement, which can be inconvenient. (I wish there was a decltype(return), which could only be used in return statements, to tell us the return type. Allowing return decltype(return)({1,2,3});)



PS: calling functions is slightly more annoying than in needs to be, when it comes to cleanly selecting the desired constructor of the parameter. I wish it was possible to calling a function like this:

// wish-list code, not supported
foo(
    {}      // create first arg with default constructor
   ,{1}     // create second arg with one-arg non-init-list constructor
   ,{5,'x'} // if the third arg is of type vector<char>, for example
   ,{{}}    // construct fourth arg with an empty init list
   ,{{1}}   // construct fifgh arg with an one-element init list
   ,{{1,2}} // construct sixth arg with a two-element init list
);

But, this is impossible now. With a time machine, and a small change to the original spec, this could all be a lot cleaner.

Upvotes: 0

Nicol Bolas
Nicol Bolas

Reputation: 473517

If you want the init-list constructor, do X1,Y1,Z1.

The only way to guarantee, for an arbitrary type, the use of an initializer_list constructor (with compilation failure if no such constructor exists) is to actually specify that to the braced-init-list:

T v(std::initializer_list<int>{...});

If you didn't have the std::intializer_list part there, then your braced-init-list might have initialized the first parameter as being of some other type.

But if you don't want it, then do X2,Y2,Z2.

The only way to guarantee, for an arbitrary type and for arbitrary arguments, the calling of a non-initilaizer_list constructor is to not use braced-init-lists at all. std::allocator<T>::construct does this, for example.

T v(...);

If you use a braced-init-list at all, for an arbitrary type, then you run the risk of calling the wrong kind of constructor.

If you read code that doesn't clearly follow one of these patterns exactly, then things get complicated and you could consider rewriting

That's up to you, but the fact is other people are not going to give up braced-init-lists just because they might be confusing in some corner cases.

Remember: the problems you're talking about only arise when dealing with an unknown type: some arbitrary T or a container of an arbitrary T or whatever. While there is plenty of template code that deals in arbitrary types T, there's plenty of code that doesn't as well. Most of the time, it is very well understand what a particular braced-init-list will do.

So no, there is no rule which, if not followed, "you could consider rewriting" the code in question. Other people are not going to follow these draconian rules, simply because there are places where you have to be careful when using braced-init-lists.

Now, do I wish that we had a language feature that would allow you to specify if a braced-init-list could call an initializer_list constructor or not? Absolutely. But that ship sailed 5 years ago, and tons of code has been written since then.

If we had to write a macro to take a type T, a variable name v, and a set of parameters, and you wanted to avoid the init-list constructor and also avoid the most vexing parse, how would we write it?

The most-vexing-parse primarily shows up because you want to default-construct some temporary:

K k(T()); //Declares a function.

Therefore, if you wish to avoid this, use braced-init-lists for default construction:

K k(T{});

So users of your macro will have to do the same. That is, it is on the user to use {} for any default-constructed temporary objects. There is nothing your macro can do to prevent it.

Upvotes: 3

Related Questions