11

EDIT: Before we begin, this question is not about proper usage of std::initializer_list; it is about what should be passed when the convenient syntax is desired. Thank you for staying on topic.


C++11 introduces std::initializer_list to define functions accepting braced-init-list arguments.

struct bar {
    bar( char const * );
    bar( int );
} dog( 42 );

fn foo( std::initializer_list< bar > args );

foo( { "blah", 3, dog } );

The syntax is nice, but under the hood it is distasteful due to various problems:

  • They cannot be meaningfully moved. The above function must copy dog from the list; this cannot be converted to move-construction or elided. Move-only types cannot be used at all. (Well, const_cast would actually be a valid workaround. If there's an article about doing so, I'd like to see it.)

  • There are no constexpr semantics, either. (This is forthcoming in C++1y. It's a minor issue, though.)

  • const does not propagate as it does everywhere else; the initializer_list is never const but its contents always are. (Because it doesn't own its contents, it cannot give write access to a copy, although copying it anywhere would seldom be safe.)

  • The initializer_list object does not own its storage (yikes); its relationship to the completely separate naked array (yikes) providing the storage is hazily defined (yikes) as the relationship of a reference to a bound temporary (quadruple yikes).

I have faith these things will be fixed in due time, but for now is there a best practice to get the advantages without hard-coding to initializer_list? Is there any literature about or analysis into working around direct dependency on it?

The obvious solution is to pass by value a standard container such as std::vector. Once the objects are copied into it from the initializer_list, it is move-constructed to pass by value, and then you can move the contents out. An improvement would be to offer storage on the stack. A good library might be able to offer most of the advantages of initializer_list, array, and vector, without even using the former.

Any resources?

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • 2
    [Have you read this already?](http://stackoverflow.com/questions/17623098/questions-regarding-the-design-of-stdinitializer-list/17623839#17623839) – Borgleader Jul 18 '13 at 05:58
  • 2
    @Borgleader Seems I covered that in the question already. Are you pointing out something in particular? – Potatoswatter Jul 18 '13 at 05:59
  • 4
    Initializer lists are so called because they are for *initializing things*. If you're not initializing something with a list of like values, then you shouldn't be taking them as parameters in your function. It's not clear why `foo` is taking an initializer list at all. It's *not a container*; that's exactly what that answer you were linked to is trying to remind you. If you want an array, that's different from wanting an initializer list. – Nicol Bolas Jul 18 '13 at 06:01
  • @NicolBolas How do you know that `foo` isn't constructing an object and storing it somewhere? You're taking a generic example and imagining it does something wrong somewhere else. Anyway, `initializer_list` semantics are wrong for containers too, that's the point. Do you want to copy an object twice before it gets into the `vector` you are initializing, or do you want the option of moving it the whole way? – Potatoswatter Jul 18 '13 at 06:03
  • 3
    @Potatoswatter: "*How do you know that foo isn't constructing an object and storing it somewhere?*" I don't. My point is that *you* are not clear why `foo` is taking an initializer list. As far as I'm concerned, the *only* kind of function that should take a `std::initializer_list` as a parameter is an object's constructor. All other functions should take some form of container with actual semantics on it. – Nicol Bolas Jul 18 '13 at 06:06
  • @NicolBolas http://en.wikipedia.org/wiki/Metasyntactic_variable and if you have nothing to add… – Potatoswatter Jul 18 '13 at 06:07
  • Note that `dog` needs the default constructor, yet your class doesn't have it! – Nawaz Jul 18 '13 at 06:08
  • 4
    @Potatoswatter: "*Anyway, initializer_list semantics are wrong for containers too, that's the point.*" That all depends on what you're using them for. If it's an array of integers, there's no problem. If it's an array of heavier-weight objects, then it's your own fault for using the wrong tool for the job. Also, you only get one copy; you can move *into* the initializer_list object's storage just fine. As with any other user-elected moves, you must use `std::move` to invoke it. – Nicol Bolas Jul 18 '13 at 06:12
  • Are you sure the copies cannot be elided in any case? I thought they could not be elided if they had side effects (-> observable behaviour). Also, AFAIK, C++1y allows `initializer_list` in constant expressions. – dyp Jul 18 '13 at 06:19
  • 1
    [Bring more fuel to the fire!](http://coliru.stacked-crooked.com/view?id=291df3b9729047100ad67a8c88ed928c-0f11904895a20602a6e5ca9f4b5b0039) – Mark Garcia Jul 18 '13 at 06:26
  • 3
    @jogojapan Ah… you can move into the initializer_list so it's just one copy. But applying `move` to an `initializer_list` is nonsense. – Potatoswatter Jul 18 '13 at 06:26
  • 1
    Re the missing move-semantics: Would it help if there was a working implementation of something like `initializer_list`? I.e. something that assumes that _all_ elements should be regarded as movable rvalues? Or does it have to be a mechanism that somehow remembers (at compile time) which element was passed as rvalue and which as lvalue, and then applies move semantics only to the former? – jogojapan Jul 18 '13 at 06:48
  • 1
    @jogojapan Yeah, I was just thinking of something like a wrapper around `initializer_list` which provides move iterators from its `begin()` and `end()`. But this question is more about whether such a thing exists in an existing, documented library, because I don't actually have a need right now but just don't want to write code that will be stale. – Potatoswatter Jul 18 '13 at 07:08
  • As you realized yourself a `std::initializer_list` doesn't fit. What you want to do is pass a variable number of arguments (of more or less the same type) to a function in order to iterate them. So what is wrong with the "obvious solution" from the last paragraph (maybe using a `std::dynarray` instead of a `std::vector`)? Are you just asking if something similar already exists or if this approach still has any drawbacks or what? – Christian Rau Jul 18 '13 at 09:29
  • @ChristianRau Yep, that's the question. I haven't looked into `dynarray` and would be interested in learning its applicability. The obvious drawback of `vector` is that it's always on the heap. – Potatoswatter Jul 18 '13 at 10:08
  • 1
    @Potatoswatter Unfortunately even a `std::dynarray` is not *required* to be on the stack, but only *encouraged* in the contexts where it's possible, which a temporary should belong to. Yet better a *"might be"* than a *"cannot be"*. But wait, isn't there the same problem again. The container's (whatever it is) constructor should have the same problem as the plain `initializer_list`-accepting function. Thus that solution won't buy anything either, no? – Christian Rau Jul 18 '13 at 11:06
  • @ChristianRau Since I haven't reviewed the internal semantics of `dynarray`, I don't know. It can't be as bad as `initializer_list`, can it? Semantically, `initializer_list` does the trick as long as you apply a `const_cast` to allow moving its contents. I just want a temporary container good for passing stuff, which wasn't hastily standardized and is future-proof. `dynarray` could have defects which cause it to be deprecated, but a third-party library or idiom could stay "fresh" forever. – Potatoswatter Jul 18 '13 at 11:28
  • 1
    @Potatoswatter But the problem is that *any* container used instead of the `std::initializer_list` would have a `std::initializer_list` as an additional layer anyway. And the corresponding `std::initializer_list`-constructor isn't any different from your `foo` regarding the problems with moving and the like. So neither `std::vector` nor `std::dynarray` *can* be better than a plain `std::initializer_list` in this regard (and I'm not sure *any* library-based solution can, except for the obvious of wrapping a `std::initializer_list` and `const_cast`ing internally for the sake of evil). – Christian Rau Jul 18 '13 at 11:30
  • 1
    @ChristianRau Yeah, but literally the question is just how to avoid using `initializer_list` *explicitly* so my program automatically gets fixed whenever the underlying language does. I don't have a pressing performance need, I just want cleanliness. – Potatoswatter Jul 18 '13 at 11:32
  • @Potatoswatter *"I don't have a pressing performance need, I just want cleanliness"* - Well then the container solution is really the obvious one. Under some limitations and with some proper SFINAE support it might also be doable with variadic templates if iteration is not required, of course without the `{}` but those are more anying than helpful anyway. – Christian Rau Jul 18 '13 at 11:36
  • Please post answers instead of having really good information in comments; comments are ephemeral and may be deleted at any time. Answers stick around, are easier to follow, and have the ability to for good syntax highlighting (among other features). If it's extended discussion you're looking for, we have chat for that. – George Stocker Jul 18 '13 at 11:46
  • 4
    argh, the comment police again. – TemplateRex Jul 18 '13 at 12:19

1 Answers1

6

it is about what should be passed when the convenient syntax is desired.

If you want the convenience of size (ie: the user just types a {} list with no function calls or words), then you must accept all the powers and limitations of a proper initializer_list. Even if you try to convert it into something else, like some form of array_ref, you still have to have an intermediary initializer_list between them. Which means that you can't get around any of the issues you've run into, like not being able to move out of them.

If it goes through an initializer_list, then you have to accept these limitations. Therefore, the alternative is to not go through an initializer_list, which means that you're going to have to accept some form of container with specific semantics. And the alternative type would have to be an aggregate, so that the construction of the alternate object won't encounter the same problem.

So you're probably looking at forcing the user to create a std::array (or a language array) and passing that. Your function could take some form of array_ref class, which can be constructed from any array of arbitrary size, so the consuming function isn't limited to one size.

However, you lose the convenience of size:

foo( { "blah", 3, dog } );

vs.

foo( std::array<bar, 3>{ "blah", 3, dog } );

The only way to avoid the verbosity here is to have foo take std::array as a parameter. Which means that it could only take an array of a specific fixed size. And you couldn't use C++14's proposed dynarray, because that will use an initializer_list intermediary.

Ultimately, you shouldn't be using uniform initialization syntax for passing around lists of values. It's for initializing objects, not for passing lists of things. std::initializer_list is a class who's sole purpose is to be used to initialize a specific object from an arbitrarily long list of values of identical types. It is there to serve as an intermediary object between a language construct (a braced-init-list) and the constructor that these values are to be fed into. It allows the compiler to know to call a particular constructor (the initializer_list constructor) when given a matching braced-init-list of values.

This is the entire reason why the class exists.

Therefore, you should use the class exclusively for the purpose for which it was devised. The class exists to tag a constructor as taking a list of values from a braced-init-list. So you should use it only for constructors that take such a value.

If you have some function foo that acts as an intermediary between some internal type (that you don't want to directly expose) and a user-provided list of values, then you need to take something else as a parameter to foo. Something that has the semantics you desire, which you can then feed into your internal type.

Also, you seem to have a misconception around initializer_lists and movement. You cannot move out of an initializer_list, but you can certainly move into one:

foo( { "blah", 3, std::move(dog) } );

The third entry in the internal dog array will be move-constructed.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • I think the important issue here is the *"You cannot move out of an `initializer_list`"* part. – Mark Garcia Jul 18 '13 at 06:33
  • @Potatoswatter any link to that factory function idiom with init-lists? – TemplateRex Jul 18 '13 at 06:39
  • 3
    @Potatoswatter: "*As for the contention that initializer_list can only be used as an argument to a constructor*" I said *should*, not "can" or "could". I answered with what you *ought* to do, not what you *can* do. Idiomatic C++ should avoid using `initializer_list` outside of constructors. "*There exists a factory function idiom*" which *does not work* with uniform initialization at all. So what's your point? – Nicol Bolas Jul 18 '13 at 06:51
  • 1
    @Potatoswatter: "*If you define a function encapsulating the functionality of an initializer_list constructor*" You can't write a function that does that, so your point is moot. – Nicol Bolas Jul 18 '13 at 06:52
  • 3
    @Potatoswatter: "*I can't believe you wrote an entire answer based on the nitpick that my example uses the generic foo instead of a constructor.*" Garbage in, garbage out. If you want a particular question answered, then the examples you provide need to actually be *representative* of what you want to know. If you don't intend to use `initializer_list` outside of constructors, then don't provide an example where you do so. – Nicol Bolas Jul 18 '13 at 06:54
  • 3
    @Potatoswatter: "*you've answered that the semantics are terrible so it should seldom be used*" No, I answered that the semantics are fine *for it's intended use*, so you should limit yourself to using it for those intended uses. Namely constructing things. – Nicol Bolas Jul 18 '13 at 07:50