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:

What happens if we try to use an initializer list with this container class?

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.

Prior to C++11, list initialization could only be used with static or dynamic arrays. However, as of C++11, we now have a solution to this problem.

Class initialization using std::initializer_list

When a C++11 compiler sees an initializer list, it automatically convert 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. Therefore, you’ll 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 also pass the list by const reference, so we don’t make an unnecessary copy of the std::initializer_list when it’s passed to our constructor.

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.

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 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.

Class assignment using std::initializer_list

You can also use std::initializer_list to assign new values to a class by overriding the assignment operator to take a std::initializer_list parameter. This work 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
  3. Make the constructor explicit, so it can’t be used for implicit conversions

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 the constructor that takes a std::initializer_list isn’t marked as explicit, 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 (by reference to prevent copying) 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

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

23 comments to 10.7 — std::initializer_list

  • CrazyL

    Hello Alex,

    I got a question regarding the quiz solution (l:4):

    I understand that we need to reallocate memory if the old array was smaller than the new one. But why should we do that if the new array is smaller? Shouldn’t we then just copy it into the old memory, leaving some space at the end unused (like vector objects, when they allocate a little more space for new entries to avoid resizing next time)?

    Or am I missing some important point here?

    Regarding your statement at the end:

    [..] At the end of the assignment, the initializer list (because it is an anonymous object) goes out of scope, and our IntArray is left with a hanging pointer. [..]

    Why is the IntArray left with a hanging pointer? The constructor used "new" to allocate memory for the anonymous object, shouldn’t that memory have static duration until it’s deleted by out IntArray?

    Thank you for this great tutorial page and all your replies, that’s really helpful and very much appreciated!

    • Alex

      > Shouldn’t we then just copy it into the old memory, leaving some space at the end unused (like vector objects, when they allocate a little more space for new entries to avoid resizing next time)?

      You could! But then you’d some way differentiate the concepts of length vs capacity since the length and capacity are no longer the same. I did it this way just for simplicity of example.

      > Why is the IntArray left with a hanging pointer? The constructor used “new” to allocate memory for the anonymous object, shouldn’t that memory have static duration until it’s deleted by out IntArray?

      No. Without doing one of the above, the initializer_list is converted into a temporary IntArray, and that temporary IntArray is then shallow copied into array. At that point in time, both array and our temporary IntArray are pointing to the same m_data (due to the shallow copy!). When the temporary IntArray goes out of scope, it will delete m_data, leaving array’s m_data hanging.

      From reading the article text, it wasn’t clear what IntArray I was talking about in some cases. I’ve updated the wording to make it easier to follow.

  • Omri

    const "does it again":

    int count = 0;
    for (auto &element : list)
    { m_data[count] = element; ++count;}
    **Works fine**

    int count = 0;
    for (int element : list)
    { m_data[count] = element; ++count;}
    **Works fine**

    int count = 0;
    for (int &element : list)
    { m_data[count] = element; ++count;}
    !!Does not compile!!
    Perhaps:
    "element" is part of the "list".
    The "list" is an r-value.
    Thus "element" is an r_value.
    Thus cannot be referenced "normally" (but rather as a const… see below).
    The auto prefix seems take care of this "on its own".

    int count = 0;
    for (const int &element : list)
    { m_data[count] = element; ++count;}
    **works fine**

  • Omri

    Hello Alex,
    Regarding:
    "Note that we also pass the list by const reference, so we don’t make an unnecessary copy of the std::initializer_list when it’s passed to our constructor."
    My understanding up to now is as follows:
    We pass the list as reference (&) so we do not make a copy of the std::initializer_list object as it is passed to the "list" based constructor.
    The "const" is required since, in this case, the std::initializer_list object does not have an "ordinary" addressable location and thus in order to reference it the compiler needs to arrange for dedicated "means" as per const objects.
    Pls advise.

  • Zeyd

    Hello,

    Good job sir,
    Thank you very much for this impeccable tutorial.

    Best regards.

  • Alexander Kindel

    When I tried implementing the overloaded assignment operator to take an initializer list, I didn’t make the std::initializer_list parameter const. My array ended up with garbage values in it by the time the program printed it the second time, and it took me a while of comparing with the answer provided to figure out that the omission of const was the reason. I thought the only thing const does in this context is alter what one is allowed to do with the value from the parameter, so that as long as a program compiles when the const is there, removing it won’t change the functionality of the program. Apparently that’s wrong in this case?

    • Alex

      I’m not sure. Since initializer lists are normally temporary r-values, I’d think if you wanted the std::initializer_list parameter to be a reference, you’d need to make it a const reference, since non-const references can’t bind to r-values. When I tried removing the const from the parameter in the IntArray example, it wouldn’t even compile (as I expected).

  • Erik

    When I try to use this, I get an error saying there is no member named initializer_list in std. I am doing "#include <initializer_list>", so I don’t think that’s the problem, but I don’t know what else it could be.

  • Jacco

    Hi Alex,

    I used the following code for overloading the assignment operator which worked fine as well:

    Thx for all your efforts, great learning, sometimes I struggle loading the website…

  • Rohit

    Why did u write m_data = 0 instead of m_data = nullptr here:
    void erase()
        {
            delete[] m_data;
            // We need to make sure we set m_data to 0 here, otherwise it will
            // be left pointing at deallocated memory!
            m_data = 0;
            m_length = 0;
        }

    and whats the difference in delete m_data    and      delete[] m_data? I want to know the difference in
    delete m_data;

    // reallocate array
    m_length = list.size();
    m_data = new int[m_length];
    }

    and

    delete[] m_data;
    // We need to make sure we set m_data to 0 here, otherwise it will
    // be left pointing at deallocated memory!
    m_data = 0;
    m_length = 0;

    • Alex

      0 and nullptr are synonymous in this context. Using nullptr is probably slightly better, but I wrote these examples before nullptr existed and din’t update them.

      delete m_data assumes m_data is not an array. delete[] m_data assumes m_data is an array. You need to call the right version of delete depending on whether m_data is an array or not.

  • Hugh Mungus

    Hey Alex,

    This is my initial solution for the quiz. Can you tell me why it doesn’t work? I run into runtime errors and it appears to be accessing memory it doesn’t own.

  • Hugh Mungus

    std::initializer_list lives in the header.

    Was that line a typo?

  • Terty

    Regarding the following code:

    Assume we use the following to create an IntArray object:

    Is the initializer_list not an anonymous object, if it is, wouldn’t assigning the array elements of the IntArray object to references to the values in the initializer_list cause problems once the constructor is finished and the initializer_list gets destroyed?

    • Terty

      I think I understand now, since m_data holds int values and not int& values the values of the elements get copied from the initializer_list even if they are references.

      • Alex

        Yes -- but when you say “even if they are references”, the only reference here is m_list, which is a const reference to the temporary initializer list argument. The elements of the initializer list are actual values, and the elements of m_data are actual values.

    • Alex

      Good question. The initializer list _is_ an anonymous object, so &list is a const reference to an anonymous object (which is okay). But note that the elements of m_data are not references -- they’re actual integer values. So when we say m_data[count] = element, this is actually _copying_ an integer value from the initializer list to an m_data element (not initializing a reference) That way, when the initializer list goes out of scope at the end of the initialization expression, we still have our copied values in m_data.

Leave a Comment

Put C++ code inside [code][/code] tags to use the syntax highlighter