Search

10.7 — std::initializer_list

Consider a fixed array of integers in C++:

If we want to initialize this array with values, we can do so directly via the initializer list syntax:

This prints:

5 4 3 2 1

This also works for dynamically allocated arrays:

In the previous lesson, we introduced the concept of container classes, and showed an example of an IntArray class that holds an array of integers:

This code won’t compile, because the IntArray class doesn’t have a constructor that knows what to do with an initializer list. As a result, we’re left initializing our array elements individually:

That’s not so great.

Class initialization using std::initializer_list

When a compiler sees an initializer list, it automatically converts it into an object of type std::initializer_list. Therefore, if we create a constructor that takes a std::initializer_list parameter, we can create objects using the initializer list as an input.

std::initializer_list lives in the <initializer_list> header.

There are a few things to know about std::initializer_list. Much like std::array or std::vector, you have to tell std::initializer_list what type of data the list holds using angled brackets, unless you initialize the std::initializer_list right away. Therefore, you’ll almost never see a plain std::initializer_list. Instead, you’ll see something like std::initializer_list<int> or std::initializer_list<std::string>.

Second, std::initializer_list has a (misnamed) size() function which returns the number of elements in the list. This is useful when we need to know the length of the list passed in.

Let’s take a look at updating our IntArray class with a constructor that takes a std::initializer_list.

This produces the expected result:

5 4 3 2 1

It works! Now, let’s explore this in more detail.

Here’s our IntArray constructor that takes a std::initializer_list<int>.

On line 1: As noted above, we have to use angled brackets to denote what type of element we expect inside the list. In this case, because this is an IntArray, we’d expect the list to be filled with int. Note that we don’t pass the list by const reference. Much like std::string_view, std::initializer_list is very lightweight and copies tend to be cheaper than an indirection.

On line 2: We delegate allocating memory for the IntArray to the other constructor via a delegating constructor (to reduce redundant code). This other constructor needs to know the length of the array, so we pass it list.size(), which contains the number of elements in the list. Note that list.size() returns a size_t (which is unsigned) so we need to cast to a signed int here. We use direct initialization, rather than brace initialization, because brace initialization prefers list constructors. Although the constructor would get resolved correctly, it’s safer to use direct initialization to initialize classes with list constructors if we don’t want to use the list constructor.

The body of the constructor is reserved for copying the elements from the list into our IntArray class. For some inexplicable reason, std::initializer_list does not provide access to the elements of the list via subscripting (operator[]). The omission has been noted many times to the standards committee and never addressed.

However, there are easy ways to work around the lack of subscripts. The easiest way is to use a for-each loop here. The for-each loops steps through each element of the initialization list, and we can manually copy the elements into our internal array.

One caveat: Initializer lists will always favor a matching initializer_list constructor over other potentially matching constructors. Thus, this variable definition:

would match to IntArray(std::initializer_list<int>), not IntArray(int). If you want to match to IntArray(int) once a initializer_list constructor has been defined, you’ll need to use copy initialization or direct initialization.

Class assignment using std::initializer_list

You can also use std::initializer_list to assign new values to a class by overloading the assignment operator to take a std::initializer_list parameter. This works analogously to the above. We’ll show an example of how to do this in the quiz solution below.

Note that if you implement a constructor that takes a std::initializer_list, you should ensure you do at least one of the following:

  1. Provide an overloaded list assignment operator
  2. Provide a proper deep-copying copy assignment operator

Here’s why: consider the above class (which doesn’t have an overloaded list assignment or a copy assignment), along with following statement:

First, the compiler will note that an assignment function taking a std::initializer_list doesn’t exist. Next it will look for other assignment functions it could use, and discover the implicitly provided copy assignment operator. However, this function can only be used if it can convert the initializer list into an IntArray. Because { 1, 3, 5, 7, 9, 11 } is a std::initializer_list, the compiler will use the list constructor to convert the initializer list into a temporary IntArray. Then it will call the implicit assignment operator, which will shallow copy the temporary IntArray into our array object.

At this point, both the temporary IntArray’s m_data and array->m_data point to the same address (due to the shallow copy). You can already see where this is going.

At the end of the assignment statement, the temporary IntArray is destroyed. That calls the destructor, which deletes the temporary IntArray’s m_data. This leaves our array variable with a hanging m_data pointer. When you try to use array->m_data for any purpose (including when array goes out of scope and the destructor goes to delete m_data), you’ll get undefined results (and probably a crash).

Rule

If you provide list construction, it’s a good idea to provide list assignment as well.

Summary

Implementing a constructor that takes a std::initializer_list parameter allows us to use list initialization with our custom classes. We can also use std::initializer_list to implement other functions that need to use an initializer list, such as an assignment operator.

Quiz time

Question #1


Using the IntArray class above, implement an overloaded assignment operator that takes an initializer list.

The following code should run:

This should print:

5 4 3 2 1 
1 3 5 7 9 11

Show Solution


10.x -- Chapter 10 comprehensive quiz
Index
10.6 -- Container classes

150 comments to 10.7 — std::initializer_list

  • giang

    Is this okay if I use void in 'void operator= (std::initializer_list<int> list)' instead of 'IntArray&' and 'return *this;'??

    • giang

      Sorry for asking a quite dumb question!! Now I remember that we should use 'IntArray&' instead in order to continue using an IntArray in the chaining cases :v

  • koe

    My quiz answer, a simple way to provide both the copy constructor and list initializer via overloaded assignment operator. Using '*this = arr' and 'reallocate' based on comments by other people.

  • Mohammed

    Why I use the default keyword in the line

    ?

    • nascardriver

      It tells the compiler to generate a default constructor, so that we can do

      The `std::initializer_list` constructor alone would be enough to do this, but it'd do unnecessary work.

  • Şefik Palazoğlu

    I implemented the quiz like this:

    I used the reallocate() function from the previous lesson.
    I hope this is a proper/correct implementation.
    I compiled it and the code produced correct outputs.
    Feedback would be appreciated!
    Thank you for these lessons!

  • Rohit Alawadhi

    Can you explain this in little more detail with some reasoning/examples.

    On line 1: As noted above, we have to use angled brackets to denote what type of element we expect inside the list. In this case, because this is an IntArray, we’d expect the list to be filled with int. Note that we don’t pass the list by const reference. Much like std::string_view, std::initializer_list is very lightweight and copies tend to be cheaper than an indirection.

    On line 2: We delegate allocating memory for the IntArray to the other constructor via a delegating constructor (to reduce redundant code). This other constructor needs to know the length of the array, so we pass it list.size(), which contains the number of elements in the list. Note that list.size() returns a size_t (which is unsigned) so we need to cast to a signed int here. We use direct initialization, rather than brace initialization, because brace initialization prefers list constructors. Although the constructor would get resolved correctly, it’s safer to use direct initialization to initialize classes with list constructors if we don’t want to use the list constructor.

    • nascardriver

      You quoted a lot of text there. What is it that you don't understand?

      • Rohit Alawadhi

        Point 1, I read again and saw comments and understood.
        Point 2: This line:
        We use direct initialization, rather than brace initialization because brace initialization prefers list constructors. Although the constructor would get resolved correctly, it’s safer to use direct initialization to initialize classes with list constructors if we don’t want to use the list constructor.

        What is list constructor? Is it constructor delegation or it is just other name for member initialization list?
        I will only be able to question the next line once I understand what is this in the first place.

        Do you have some notifier for the answers you post on our comments? For instance, email?

        I didn't realise I had answers on my comments!

        • nascardriver

          A list construct is a constructor that takes a `std::initializer_list`. You receive email notifications when someone replies to your comment. Check your spam folder.

          • Rohit Alawadhi

            We use direct initialization, rather than brace initialization because brace initialization prefers list constructors.

            If brace initialisation prefers list constructors than why do you use direct initilisation?

            In the next line you counter brace by saying direct initialisation is safer. Even the next part of this line is confusing.

            And I checked my spam, it wasn't there as well!

            • nascardriver

              `IntArray` has these constructors

              This causes list initialization and direct initialization to have different effects

              In the example, we want to call `IntArray(int)`, so we need to use direct initialization.

              Seeing that you use a common email provider, which works for others, I don't think the error is on our end. I told Alex about your notification problem for further investigation.

  • AbraxasKnister

    I think this is a good point to ask a few things I wondered about.

    Do "brace initialization", "uniform initialization" and "list initialization" all refer to the same thing (and if so, what else falls into that category)?

    Why is the compiler able to cast anything that is a comma separated list in braces into a std::initializer_list even though we never told him what that is by #including the relevant header? I see why this works for certain user defined types that included the header. But why does it work for `int i{ 0 };` and similar?

    If the compiler magically knows the std namespace and the initializer_list type within it without the need of including it, how can I know what names/types the compiler can see at a certain point? For example in python there is a 'dir()' function that lists the names accessible from a certain object/module (and if you didn't give anything it lists the names in the scope).

    If the compiler magically knows about stuff in the standard library, can I use C++ without it using the standard library under the hood?

    I understand if the second last points are too far fetched to answer them here. I guess the best thing would be if I read into the details of the compilation process/environment.

    • nascardriver

      > "brace initialization", "uniform initialization" and "list initialization"

      The marked part is a "braced-init-list". We're using the "braced-init-list" to to initialize an object (I don't think there's another use to them). That makes it an "initializer list". The whole thing is called "list-initialization".
      Those are the official names from the standard (9.4.4 dcl.init.list).
      It's also called "brace initialization", because there are braces. The term "List initialization" can be confusing, because often there's only 1 element in the list.
      It's also called "uniform initialization", because it's universal (Works almost everywhere).
      `std::initializer_list` is a different story. Initializer lists can exists without `std::initializer_list` (Confusing naming).

      > Why is the compiler able to cast anything that is a comma separated list in braces into a std::initializer_list
      You can't use `std::initializer_list` without #including <initializer_list>.

      `auto` (That includes the for-range-initializer) gets special treatment from the standard. It tries to be a `std::initializer_list`. This behavior can't be created in code (ie. you can't create a type where `auto` does this, it only works for `std::initializer_list`, because the standard says so).

      > how can I know what names/types the compiler can see at a certain point?
      You can only use what you include. If you try to use something but you're missing the include, you won't be able to compile. The only weird thing I know of is `typeid`. Although it's a keyword, you're not allowed to use it without including (But of course there is no declaration of `typeid` in , because `typeid` is a keyword).

      > If the compiler magically knows about stuff in the standard library, can I use C++ without it using the standard library under the hood?
      I don't know if the standard enforces the availability of the standard library. There are compilers where you don't have a standard library, eg. when developing for Arduino.

      • AbraxasKnister

        Thanks, that cleared the ambiguities. Here's an example I thought of:

  • AbraxasKnister

    > One caveat: [...] Thus, this variable definition: IntArray array { 5 };
    would match to IntArray(const std::initializer_list<int> &), not IntArray(int).

    The initializer list constructor was explicitly declared to copy the list by value. (why?)

    • nascardriver

      `std::initializer_list` is very fast to copy (Because it doesn't store the elements directly). So fast indeed, that it can be faster to copy an `std::initializer_list` than passing it by reference (Because the reference requires an indirection). See sAy's comment https://www.learncpp.com/cpp-tutorial/10-7-stdinitializer_list/comment-page-2/#comment-457145

      The sentence you quoted was missing during the update, it should say "IntArray(std::initializer_list)", not "IntArray(const std::initializer_list &)". I updated it in the lesson, thanks!

  • sAy

    According to this StackOverflow question (https://stackoverflow.com/questions/17803475/why-is-stdinitializer-list-often-passed-by-value), it seems that std::initializer_list is usually implemented as a pair of pointers. cppreference (https://en.cppreference.com/w/cpp/utility/initializer_list) also says that an object of this type is lightweight. Therefore, passing std::initializer_list by const value is probably almost as efficient as passing by const reference, and it can save a dereferencing (negligible, though).

    • nascardriver

      Thanks for bringing this to our attention! I updated the lesson to pass `std::initializer_list` by value. This change might affect lessons after this. If you see `std::initializer_list` being passed by reference in another lesson, feel free to point it out :)

  • sito

    hello! this question might have already been answered and if it was I'm sorry for asking again.
    How do you go about implementing an initializer list for a user defined type?

  • Ged

    I know this code can have an improvement and not change if the length is the same.

    Could you explain this warning?
    Warning    C6386    Buffer overrun while writing to 'm_data':  the writable size is 'm_length*4' bytes, but '8' bytes might be written.

  • cnoob

    hi! In the Quiz line 43:

    If the std::initializer_list is not const, the compiler says that Im refering to an explicitly deleted function. Why? I know that we dont want to change the list so it should be const, but why cant the compiler tell the deleted operator from the other without the new one being const, even though their parameters are different anyway?

    • nascardriver

      can't bind to an r-value initializer list, so the constructor ignores this function.

      The compiler has to find some other way of compiling

      The only other way is to construct an `IntArray` from the initializer list, then use `operator=(const IntArray& list)`. That would work, but this operator is deleted.

  • In the quiz exercise, why can't we just call the constructor that takes ::initializer_list as a parameter, instead of using another loop in the implementation of the assignment operator?

  • Ryan

    For example if we had.

    This would just recopy the same numbers into the list again! I'd say that's inefficient.
    There is no implicit conversion for an int array to an initializer list so (this == &list) would not work.

    The function can be changed a bit to improve the efficiency.

    However I was wondering, how we could do an overloading type to std::inilizer_list such that if we did,

    it would work.

    • nascardriver

      Self-assignments are extremely rare. We're checking for self-assignments to prevent errors, not for efficiency.
      Without testing, I'd say your code is less efficient than an implementation that copies the values unconditionally. A quick test confirms my assumption, at least on my system.
      For custom types, your version prevents the type from doing its own self-assignment check, which might be a lot faster than yours.

      This can never work, because you're comparing pointers of different types. You can use `std::equal` to compare lists.

      • Ryan

        But isn't self-assignment considered dangerous or time wasting in assignment operators 9.14.
        I know that
        if (length != m_length)
        pretty much prevents the object to point to a garbage address, such that it ensures an identical object's array does not get go out of the heap. Am i correct?
        So comparable to what I did, what does the compiler do in it's own self-assignment check.

        Also out of curiosity, would a copy and swap idiom algorithm be an good alternative?

        • nascardriver

          Self-assignment is dangerous and time-wasting. You can fix the dangerous part, but not the time-wasting part. The only real way to not waste time is to not assign an object to itself in the first place.

          > if (length != m_length)
          This check is optional. If the old and new array have the same length, we can re-use the memory. The check could also be `if (length <= m_length)`. That way we'd waste some memory, but we don't have to re-allocate. > what does the compiler do in it's own self-assignment check
          There's no self-assignment check in the compiler. Built-in types don't need self-assignment checks, because a self-assignment isn't dangerous for them.

          > would a copy and swap idiom algorithm be an good alternative?
          Yes. If I recall correctly, Alex wants to integrate this into the lessons.

  • hellmet

    Hmm I think I'm missing a nuance here. One prefers uniform initialization to prevent copies. But, in the std::initializer_list constructor, I'm copying the elements one by one. I can't think of a way to prevent those copies though. We have n copies here, else, without uniform init, we'd have 2n copies. Well better this than 2n I guess!

    Can't I cheat by assigning references to the elements in the constructor? Lifespan extension through reference? But I think that was only for r-value const references though, so the 'cheat' wouldn't work here, unless the array itself as const).

  • Ken

    In the solution of the quiz, when you return *this do you return pointer to the entire class?

  • sareo

    Hi!
    My original answer to this quiz was:

    And I get a warning "Buffer overrun while writing to 'm_data':  the writable size is 'm_length*4' bytes, but '8' bytes might be written."    
    When I replace lines 3-5 with your answer, that warning goes away.

    I'm not understanding what is relevantly different between the two. Can you tell me why that warning is occurring, and how the second piece of code fixes it?

  • Atas

    "Rule: If you provide list construction, it’s a good idea to provide list assignment as well"

    Why should one prefer overloading list assignment over regular assignment? The latter seems more general, isn't that's a good thing?

    • If you provide a list constructor but not a list assignment operator and you assign a list, you're calling the list constructor and then the copy/move constructor, which is slower than immediately calling a list assignment operator.

  • Dimbo1911

    Good morning, how does this code seem?

    • Good day,
      - Line 18: Initialize in the member initializer list.
      - Line 22: Brace initialization.
      - Line 24-29, 50+: Can be replaced with `std::copy(list.begin(), list.end(), m_data);`. This isn't covered on learncpp.
      - `IntArray::getLength` should be const.
      - Line 76, 82: Use single quotation marks for characters.

  • Abrar Rahman Protyasha

    Hey, just a quick question about the quiz solution:

    Would we not see undefined behavior in this section if someone passes an empty initializer list {}, where list.size() = 0? I'm just not sure what the behavior of new [] is when the size passed to new is 0.

    • Abrar Rahman Protyasha

      Actually, I didn't think through the if clause inside, I answered myself here. Regardless, could you please tell me what's the return value when we call new int[0]? I would assume it's nullptr but then you're manually assigning nullptr again so I'm not sure. Thanks again!

      • 0-sized arrays are legal when dynamically allocating arrays (But not for static arrays!).
        @new will return the address of the newly allocated empty array.
        Although empty arrays are not particularly useful, they won't crash.
        Line 5 of your quoted code doesn't do anything, since @m_arr is a @nullptr at that point already.

  • Deepak

    Hi,
    Suppose i have a class with below variables

    How to Initialize both while creating the object

    I tried writing the constructor as below,but getting error.

    could you please tell me how this can be handled

    • @list1 is an @std::initializer_list. You're missing the template argument, and '100' isn't an initializer list.

      You don't need an initializer list at all to make your call work.

  • Pawan Kataria

    Hi,

    While studying delegating constructors(https://www.learncpp.com/cpp-tutorial/8-6-overlapping-and-delegating-constructors/), I read,

    "A few additional notes about delegating constructors. First, a constructor that delegates to another constructor is not allowed to do any member initialization itself. So your constructors can delegate or initialize, but not both."

    But here we are delegating from one constructor to another to allocate memory and then initializing it members in the constructor which just did a delegation.

    Which part I understood wrong ??

    • Hi!

      The constructor's body can do whatever it wants, but there can not be any initializations in the member initializer list.

  • Anthony

    Hi Alex,

    Just to be clear: If the copy constructor and the assignment operator are not deleted, then they should (both) be overloaded and the initializer constructor made explicit? I've done this below:

    • Alex

      No. If you have a constructor that takes a std::initializer_list, you should have a corresponding list-assignment or a copy assignment function. Otherwise you'll get a shallow copy if you do a list assignment.

      The prior recommendation to make your std::initializer_list constructor explicit doesn't prevent this by itself, so that recommendation has been revoked, but it's a good idea to mark all single-parameter constructors as explicit anyway, so no harm there.

      • Anthony

        > No. If you have a constructor that takes a std::initializer_list, you should have a corresponding list-assignment or a copy assignment function. Otherwise you'll get a shallow copy if you do a list assignment.

        But if you're too lazy to have the corresponding list-assignment or copy assignment functions, you should delete them so that they can't be used to shallow copy by accident?

  • Louis Cloete

    @Alex, your solution again wouldn't compile with the stricter compiler error levels you recommended in 0.11. Here is what you should change:

    In the IntArray(const std::initializer_list &list) constructor, you need to call the delegating constructor with IntArray(static_cast<int>(list.size()), else you will get a -Werror warning treated as an error about a narrowing conversion which might change the sign. Thus:

    Likewise in the operator=(const std::initializer_list &list) method. I solved the problem like this:

    Further, I get a -Werror warning saying the class implements pointer members, but doesn't override IntArray(const IntArray&) or operator=(const IntArray&). I did this:

    and the compiler was happy!

    • Alex

      Thanks, and updated. I'm sure there are other instances of examples that were compiled before the new settings recommendations were put in place. If you find other examples that generate warnings, please point them out so I can update them. Much appreciated!

  • hassan magaji

    hi Alex,
    about the following code(i might be wrong):

    since copying fundamental data types is faster than referencing them.

  • Jon

    Hello! On the way to figuring out the quiz I had a question I couldn't quite answer. I originally had my operator overload function written like the following, which does print the expected output but also results in a runtime error because it accidentally results in the class Destructor trying to delete unallocated memory later on. I understand how to fix it and get the program to run correctly.

    My question is, with the following INCORRECT code, since I delete[] m_data and free the dynamically allocated memory but forget to reallocate the m_data array via the "new" keyword, where and how is my for-each loop setting up the (1, 3, 5, 7, 9, 11) array that still prints correctly?

    In a new memory address on the stack, instead of the heap as originally intended? Or is it able to still store and retrieve values in/from the same heap addresses even though they remain unallocated (which doesn't make sense to me)? I just want to make sure I understand what's really going on under the hood. Thank you!

Leave a Comment

Put all code inside code tags: [code]your code here[/code]