103

Why isn't std::initializer_list a core-language built-in?

It seems to me that it's quite an important feature of C++11 and yet it doesn't have its own reserved keyword (or something alike).

Instead, initializer_list it's just a template class from the standard library that has a special, implicit mapping from the new braced-init-list {...} syntax that's handled by the compiler.

At first thought, this solution is quite hacky.

Is this the way new additions to the C++ language will be now implemented: by implicit roles of some template classes and not by the core language?


Please consider these examples:

   widget<int> w = {1,2,3}; //this is how we want to use a class

why was a new class chosen:

   widget( std::initializer_list<T> init )

instead of using something similar to any of these ideas:

   widget( T[] init, int length )  // (1)
   widget( T... init )             // (2)
   widget( std::vector<T> init )   // (3)
  1. a classic array, you could probably add const here and there
  2. three dots already exist in the language (var-args, now variadic templates), why not re-use the syntax (and make it feel built-in)
  3. just an existing container, could add const and &

All of them are already a part of the language. I only wrote my 3 first ideas, I am sure that there are many other approaches.

emesx
  • 12,555
  • 10
  • 58
  • 91
  • 26
    The standards committee *hate* adding new keywords! – Alex Chamberlain Mar 04 '13 at 10:01
  • 11
    This I understand, but there are many possibilities how to extend the language (*keyword was just an example*) – emesx Mar 04 '13 at 10:02
  • So you would be ok with `std::array` but not with `std::initializer_list`? They have different semantics, so that wouldn't work. – K-ballo Mar 04 '13 at 16:48
  • 10
    `std::array` is no more 'part of the language' than `std::initializer_list`. And these are not nearly the only library components that the language relies on. See `new`/`delete`, `type_info`, various exception types, `size_t`, etc. – bames53 Mar 04 '13 at 16:49
  • 6
    @Elmes: I would have suggested `const T(*)[N]`, because that behaves very similarly to how `std::initializer_list` works. – Mooing Duck Mar 04 '13 at 17:51
  • 1
    [This](http://stackoverflow.com/questions/7108425/why-is-the-size-not-a-template-argument-of-stdinitializer-list) answers why `std::array` or a statically-sized array are less desirable alternatives. – boycy Mar 07 '13 at 09:33
  • I read all comments and answers, and found exactly the same discussion here: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2215.pdf If anyone is interested, pls check "4.5 The initializer_list class" • Is initializer_list a keyword? No, but. • Must I #include a header to use initializer_list? Yes, #include • Why don’t we use a constructor that takes a general STL sequence? • Why don’t we use a general standard library class (e.g. Range or Array)? • Why don’t we use T(&)[N]? • Why don’t we use T[N]? • Can the size() be a constant expression? Yes – Berthin Aug 16 '22 at 15:59

6 Answers6

48

There were already examples of "core" language features that returned types defined in the std namespace. typeid returns std::type_info and (stretching a point perhaps) sizeof returns std::size_t.

In the former case, you already need to include a standard header in order to use this so-called "core language" feature.

Now, for initializer lists it happens that no keyword is needed to generate the object, the syntax is context-sensitive curly braces. Aside from that it's the same as type_info. Personally I don't think the absence of a keyword makes it "more hacky". Slightly more surprising, perhaps, but remember that the objective was to allow the same braced-initializer syntax that was already allowed for aggregates.

So yes, you can probably expect more of this design principle in future:

  • if more occasions arise where it is possible to introduce new features without new keywords then the committee will take them.
  • if new features require complex types, then those types will be placed in std rather than as builtins.

Hence:

  • if a new feature requires a complex type and can be introduced without new keywords then you'll get what you have here, which is "core language" syntax with no new keywords and that uses library types from std.

What it comes down to, I think, is that there is no absolute division in C++ between the "core language" and the standard libraries. They're different chapters in the standard but each references the other, and it has always been so.

There is another approach in C++11, which is that lambdas introduce objects that have anonymous types generated by the compiler. Because they have no names they aren't in a namespace at all, certainly not in std. That's not a suitable approach for initializer lists, though, because you use the type name when you write the constructor that accepts one.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • 1
    It seems to me that this division is not possible (mailny?) because of such *implicit* roles of types. `type_info` and `size_t` are nice arguments.. well `size_t` is just a typedef.. so let's skip this. The difference between `type_info` and `initializer_list` is that the first is a result of an *explicit* operator, and the second of an *implicit* compiler action. It also seems to me, that `initializer_list` *could* be replaced with some already existing containers.. or yet better: any the user declares as argument type! – emesx Mar 04 '13 at 15:07
  • @elmes: I don't see any significant difference between what you call an explicit operator and what you call an implicit compiler action. They're both just syntax understood by the compiler, they're both "core" to the language. And I don't think it would change your question if initializer list syntax resulted in a `const std::array` (or something) instead of a `std::initializer_list`, they're both types in the standard library. If it's true that `const std::array` would do, it might even just be because they're both new and were considered separately. – Steve Jessop Mar 04 '13 at 15:11
  • 4
    ... or it might be the simple reason that if you wrote a constructor for `vector` that takes an `array` then you could construct a vector from *any* array of the right type, not just one generated by the initializer list syntax. I'm not sure it would be a bad thing to construct containers from any `array`, but it's not the intent of the committee in introducing the new syntax. – Steve Jessop Mar 04 '13 at 15:20
  • 1
    I partially agree with you. Why do you think C++ has var-args, now variadic templates and .. initializer list..? Don't you see a bit of inconsistency here? My original question's intent was to understand why the committee choose the last approach.. – emesx Mar 04 '13 at 16:36
  • @elmes: "An array of stuff" and "a list to initialize stuff with" are *fundamentally* different concepts. I see no reason why the two should share the same type. Also, wrt "any the user declares as argument type!" - if you have `int x{5};`, there is no `std::initializer_list` *ever* involved. Same goes for your types if they happen to have a matching constructor for what's written inside the braces. – Xeo Mar 04 '13 at 17:36
  • @elmes Well ok, varargs are a relict of *C* and not really needed since variadic templates are out (who used them before anyway? except for the occassional evil `printf` maybe). But with just using variadic templates, you e.g. cannot do `for(auto i : {1,2,3}) ...`, without maybe introducing some temporary type (like a `std::array(1, 2, 3)`) that, in constrast to an implicit `initializer_list`, would copy those values (no matter what your super-optimizing compiler really does). And just defaulting a `{}` to a `array` wouldn't be different from an `ilist` (except for the disadvantages mentioned) – Christian Rau Mar 04 '13 at 17:36
  • @ChristianRau: `std::initializer_list` copies all the elements just the same. – Xeo Mar 04 '13 at 17:37
  • 1
    @Xeo Really? Isn't it just a lightweight container-view into some non-specified storage area (which might need copying itself if the values are dynamically determined, but might also just be static storage like in my `{1,2,3}` example)? – Christian Rau Mar 04 '13 at 17:40
  • @Christian: No, the standard explicitly says that all arguments are copied. You also can't create an `initializer_list` of move-only types. – Xeo Mar 04 '13 at 17:42
  • @Xeo Wow, two O(n) copies for a simple `std::array a = { ... }`? That's bad news! – Christian Rau Mar 04 '13 at 17:44
  • 2
    @Christian: No, `std::array` doesn't even have any constructors. The `std::array` case is simply aggregate-initialization. Also, I welcome you to join me in the Lounge chat room, since this discussion is getting a bit long. – Xeo Mar 04 '13 at 17:45
  • 3
    @ChristianRau: Xeo means elements are copied when the initializer list is constructed. Copying an initializer list doesn't copy contained elements. – Mooing Duck Mar 04 '13 at 17:48
  • 2
    @Christian List-initialisation does not imply initializer_list. It can be many things, including good ole direct-initialisation, or aggregate initialisation. None of those involve initializer_list (and some just *cannot* work that way). – R. Martinho Fernandes Mar 04 '13 at 17:48
  • @SteveJessop this is the most complete answer so far, I accept it (for now ;-) – emesx Mar 04 '13 at 18:33
42

The C++ Standard Committee seems to prefer not to add new keywords, probably because that increases the risk of breaking existing code (legacy code could use that keyword as the name of a variable, a class, or whatever else).

Moreover, it seems to me that defining std::initializer_list as a templated container is quite an elegant choice: if it was a keyword, how would you access its underlying type? How would you iterate through it? You would need a bunch of new operators as well, and that would just force you to remember more names and more keywords to do the same things you can do with standard containers.

Treating an std::initializer_list as any other container gives you the opportunity of writing generic code that works with any of those things.

UPDATE:

Then why introduce a new type, instead of using some combination of existing? (from the comments)

To begin with, all others containers have methods for adding, removing, and emplacing elements, which are not desirable for a compiler-generated collection. The only exception is std::array<>, which wraps a fixed-size C-style array and would therefore remain the only reasonable candidate.

However, as Nicol Bolas correctly points out in the comments, another, fundamental difference between std::initializer_list and all other standard containers (including std::array<>) is that the latter ones have value semantics, while std::initializer_list has reference semantics. Copying an std::initializer_list, for instance, won't cause a copy of the elements it contains.

Moreover (once again, courtesy of Nicol Bolas), having a special container for brace-initialization lists allows overloading on the way the user is performing initialization.

Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • 4
    Then why introduce a new type, instead of using some combination of existing? – emesx Mar 04 '13 at 10:05
  • @elmes: Not sure I understand. Do you mean, "why not mapping brace lists into a vector"? – Andy Prowl Mar 04 '13 at 10:06
  • Yes, for why not vector (for example)? – emesx Mar 04 '13 at 10:07
  • @elmes: `vector` and other containers also support methods for adding, altering, and removing elements, which you do not want with an initializer list – Andy Prowl Mar 04 '13 at 10:10
  • This isn't necessarily wrong. You could always declare a `const vector` or even a `vector&&` – emesx Mar 04 '13 at 10:10
  • 3
    @elmes: Actually it is more like `std::array`. But `std::array` allocates memory while `std::initializaer_list` wraps a compile-time array. Think of it as the difference between `char s[] = "array";` and `char *s = "initializer_list";`. – rodrigo Mar 04 '13 at 10:12
  • @rodrigo: `std::array<>` also allows modifying the values it contains, like a C-style array. `initializer_list` is immutable. – Andy Prowl Mar 04 '13 at 10:13
  • 2
    And having being it a _normal_ type makes overloading, template specialization, name-decoration and the like, non-issues. – rodrigo Mar 04 '13 at 10:14
  • 2
    @rodrigo: `std::array` doesn't allocate any memory, it's a plain `T arr[N];`, the same thing that is backing `std::initializer_list`. – Xeo Mar 04 '13 at 10:25
  • 6
    @Xeo: `T arr[N]` _does allocate_ memory, maybe not in the dynamic heap but elsewhere... So does `std::array`. However a non-empty `initializer_list` cannot be constructed by the user so it obviously cannot allocate memory. – rodrigo Mar 04 '13 at 10:41
  • @elmes An `initializer_list` isn't really a container (in that it contains actual data), but rather a container-view into some (not really specified) constant storage area that holds the actual data, so it isn't like any other container around. Though, they maybe could have defined that storage area to be a `std::array` of values in the first place, but I'm not completely sure about the implications of this approach... – Christian Rau Mar 04 '13 at 17:21
  • @elmes ...But then you would have a container, which is **both** an actual container for usual container purposes **and** a special *"languagy thing"* related to a special language feature. With `std::initializer_list` you have such a special thing to be used with brace-lists, which on the other hand **cannot** be used as a regular container. Don't underestimate proper separation of concerns. – Christian Rau Mar 04 '13 at 17:24
  • 1
    "*Then why introduce a new type, instead of using some combination of existing?*" Because there is no existing type which has all of the properties of `std::initializer_list`. `vector` and `array` (and all other containers) use value-semantics; `initializer_list` uses reference-semantics. Copying an `initializer_list` does not copy its elements. Plus, if it is a separate type, it can be used in overload resolution to tell the difference between a user who use a `{}` and a user who just happened to use a `const vector`. – Nicol Bolas Mar 06 '13 at 19:53
  • So it comes down to a simple answer: a type instead of syntax, because it's safer. `initializer_list` instead of anything else, because it's more appropriate to this task. Okay. This sounds quite complete. Only question in my mind is: why the inconsistency? Why do variadic templates (or choose something else here) introduce new syntax, and uniform initialization doesn't? – emesx Mar 07 '13 at 07:09
6

This is nothing new. For example, for (i : some_container) relies on existence of specific methods or standalone functions in some_container class. C# even relies even more on its .NET libraries. Actually, I think, that this is quite an elegant solution, because you can make your classes compatible with some language structures without complicating language specification.

Spook
  • 25,318
  • 18
  • 90
  • 167
  • 2
    methods in class **or** stand-alone `begin` and `end` methods. This is a bit different IMO. – emesx Mar 04 '13 at 10:04
  • 3
    Is it? Again you have a pure language construct relying on specific construction of your code. It also might have been done by introducing new keyword, for instance, `iterable class MyClass { };` – Spook Mar 04 '13 at 10:07
  • but you can place the methods wherever you want, implement them however you want.. There is some similarity, I agree! This question is about `initializer_list` though – emesx Mar 04 '13 at 10:09
4

This is indeed nothing new and how many have pointed out, this practice was there in C++ and is there, say, in C#.

Andrei Alexandrescu has mentioned a good point about this though: You may think of it as a part of imaginary "core" namespace, then it'll make more sense.

So, it's actually something like: core::initializer_list, core::size_t, core::begin(), core::end() and so on. This is just an unfortunate coincidence that std namespace has some core language constructs inside it.

Artem Tokmakov
  • 1,135
  • 10
  • 7
2

Not only can it work completely in the standard library. Inclusion into the standard library does not mean that the compiler can not play clever tricks.

While it may not be able to in all cases, it may very well say: this type is well known, or a simple type, lets ignore the initializer_list and just have a memory image of what the initialized value should be.

In other words int i {5}; can be equivalent to int i(5); or int i=5; or even intwrapper iw {5}; Where intwrapper is a simple wrapper class over an int with a trivial constructor taking an initializer_list

Paul de Vrieze
  • 4,888
  • 1
  • 24
  • 29
  • Do we have reproducible examples of compilers actually playing "clever tricks" like this? It kinda stands to reason under _as-if_, but I'd like to see substantiation. – underscore_d Feb 01 '16 at 19:20
  • The idea of optimizing compilers is that the compiler can transform the code to any equivalent code. C++ in particular relies on optimisation for "free" abstractions. The idea of replacing code from the standard library is common (look at the gcc builtin list https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html). – Paul de Vrieze Feb 01 '16 at 19:32
  • In fact, your idea that `int i {5}` involves any `std::initializer_list` is wrong. `int` has no constructor taking `std::initializer_list`, so the `5` is just used directly to construct it. So the main example is irrelevant; there is simply no optimisation to be done. Beyond that, since `std::initializer_list` involves the compiler creating and proxying an 'imaginary' array, I guess it can favour optimisation, but that's the 'magic' part in the compiler, so it's separate from whether the optimiser in general can do anything clever with the pretty dull object containing 2 iterators that results – underscore_d Oct 27 '18 at 10:35
1

It's not part of the core language because it can be implemented entirely in the library, just line operator new and operator delete. What advantage would there be in making compilers more complicated to build it in?

Pete Becker
  • 74,985
  • 8
  • 76
  • 165