Why isn't std::initializer_list a language built-in?
Asked Answered
Z

6

104

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.

Zygospore answered 4/3, 2013 at 9:58 Comment(7)
The standards committee hate adding new keywords!Paterfamilias
This I understand, but there are many possibilities how to extend the language (keyword was just an example)Zygospore
So you would be ok with std::array but not with std::initializer_list? They have different semantics, so that wouldn't work.Insurable
std::array<T> is no more 'part of the language' than std::initializer_list<T>. 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.Diazole
@Elmes: I would have suggested const T(*)[N], because that behaves very similarly to how std::initializer_list works.Ruttger
This answers why std::array or a statically-sized array are less desirable alternatives.Chiliarch
I read all comments and answers, and found exactly the same discussion here: 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<initializer_list> • 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? YesCrocket
R
49

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.

Rimma answered 4/3, 2013 at 10:30 Comment(14)
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!Zygospore
@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.Rimma
... 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.Rimma
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..Zygospore
@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.Sacchariferous
@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)Clubwoman
@ChristianRau: std::initializer_list copies all the elements just the same.Sacchariferous
@Sacchariferous 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)?Clubwoman
@Christian: No, the standard explicitly says that all arguments are copied. You also can't create an initializer_list of move-only types.Sacchariferous
@Sacchariferous Wow, two O(n) copies for a simple std::array<int,N> a = { ... }? That's bad news!Clubwoman
@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<C++> chat room, since this discussion is getting a bit long.Sacchariferous
@ChristianRau: Xeo means elements are copied when the initializer list is constructed. Copying an initializer list doesn't copy contained elements.Ruttger
@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).Humorist
@SteveJessop this is the most complete answer so far, I accept it (for now ;-)Zygospore
N
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.

Numbersnumbfish answered 4/3, 2013 at 10:2 Comment(14)
Then why introduce a new type, instead of using some combination of existing?Zygospore
@elmes: Not sure I understand. Do you mean, "why not mapping brace lists into a vector"?Numbersnumbfish
Yes, for why not vector (for example)?Zygospore
@elmes: vector and other containers also support methods for adding, altering, and removing elements, which you do not want with an initializer listNumbersnumbfish
This isn't necessarily wrong. You could always declare a const vector or even a vector&&Zygospore
@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";.Washy
@rodrigo: std::array<> also allows modifying the values it contains, like a C-style array. initializer_list is immutable.Numbersnumbfish
And having being it a normal type makes overloading, template specialization, name-decoration and the like, non-issues.Washy
@rodrigo: std::array doesn't allocate any memory, it's a plain T arr[N];, the same thing that is backing std::initializer_list.Sacchariferous
@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.Washy
@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...Clubwoman
@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.Clubwoman
"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.Eiland
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?Zygospore
G
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.

Geanticlinal answered 4/3, 2013 at 10:2 Comment(3)
methods in class or stand-alone begin and end methods. This is a bit different IMO.Zygospore
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 { };Geanticlinal
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 thoughZygospore
M
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.

Meaningful answered 7/3, 2013 at 3:53 Comment(0)
P
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

Protein answered 8/3, 2013 at 16:48 Comment(3)
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.Gwyn
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 gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html).Protein
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 resultsGwyn
R
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?

Resurgent answered 4/3, 2013 at 13:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.