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

53 comments to 10.7 — std::initializer_list

  • nascardriver

    Hi Alex!

    Is there any reason not to initialize @m_data in the member initializer list?

  • nascardriver

    Hi Alex!

    You should add a cast to line 44 in the solution.

  • mctitkez

    Line 41.
    If don't write "const" in argument I will have an inexplicit result.

    Result on my computer:
    5 4 3 2 1
    -572662307 -572662307 -572662307 -572662307 -572662307 -572662307

    Why it's do that?

    • nascardriver

      Hi mctitkez!

      This isn't caused by not using the "const" keyword. You must have changed something else. Please share your full code.

      • mctitkez

        it's copyed code from above quiz solution. I'm tryed it again at home with other computer (both have visual studio 2017) and I get the same result... just delete "const" keyword from overload operator "="

        #include "stdafx.h"
        #include <cassert> // for assert()
        #include <initializer_list> // for std::initializer_list
        #include <iostream>

        class IntArray {
        private:
            int m_length;
            int *m_data;

        public:
            IntArray() :
                m_length(0), m_data(nullptr) {}

            IntArray(int length) :
                m_length(length) {
                m_data = new int[length];
            }

            IntArray(const std::initializer_list<int> &list) : // allow IntArray to be initialized via list initialization
                IntArray(list.size()) // use delegating constructor to set up initial array
            {
                // Now initialize our array from the list
                int count = 0;
                for (auto &element : list) {
                    m_data[count] = element;
                    ++count;
                }
            }

            ~IntArray() {
                delete[] m_data;
                // we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
            }

            IntArray& operator=(std::initializer_list<int> &list) {
                // If the new list is a different size, reallocate it
                if (list.size() != m_length) {
                    // delete any existing elements
                    delete[] m_data;

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

                // Now initialize our array from the list
                int count = 0;
                for (auto &element : list) {
                    m_data[count] = element;
                    ++count;
                }

                return *this;
            }

            int& operator[](int index) {
                assert(index >= 0 && index < m_length);
                return m_data[index];
            }

            int getLength() { return m_length; }
        };

        int main() {
            IntArray array { 5, 4, 3, 2, 1 }; // initializer list
            for (int count = 0; count < array.getLength(); ++count)
                std::cout << array[count] << ' ';

            std::cout << '\n';

            array = { 1, 3, 5, 7, 9, 11 };

            for (int count = 0; count < array.getLength(); ++count)
                std::cout << array[count] << ' ';

            return 0;
        }

        • nascardriver

          Yep, sorry, I was wrong.

          When you remove the "const"

          can no longer invoke @operator=, because the initializer list cannot be converted to a non-const initializer list. What happens instead is @IntArray(const std::initializer_list<int> &list) is called to construct a new @IntArray followed by a shallow copy of @m_data being performed into @array. That new @IntArray is then destructed, deleting @m_data and @array is left with a dangling pointer.

          • mctitkez

            Awesome. Thanks for explaining. Gods will keep a warm place in heaven for you 🙂

            But I can't understand what's different between const and non const initializer list for compiler?

            • nascardriver

              Here's an easier example

              If we were to declare @i const the compiler would know that @myFunction doesn't try to modify any arguments. So if @i was const we could pass 5, of course we wouldn't be able to assign a new value to @i in @myFunction.

              • mctitkez

                One more try to understand 🙂
                As I see inside the initialize_list's code, the initialize_list have a constructor with a "constexpr" keyword:

                constexpr initializer_list(const _Elem *_First_arg,
                    const _Elem *_Last_arg) _NOEXCEPT
                    : _First(_First_arg), _Last(_Last_arg)
                    {
                    }

                As I can understood we must pass a const ref for "constexpr". If we don't, the compliler find it in "IntArray(const std::initializer_list<int> &list)" instead "IntArray& operator=(std::initializer_list<int> &list)"?

                am I right? (if yes I need 100500 applause 🙂 )

                And from where compiller get "-572662307". m_data it's a dangling pointer? and step by step it return the same result on other computers, again and again.

                • nascardriver

                  constexpr is different from const in that it is evaluated at compile-time. The @std::initializer_list constructor isn't of interest here, because the same would happen with any other type. But you've got the important part right. If the compiler doesn't find a fitting operator= it will do the next best thing, which in this case is creating a new IntArray and copying contents.

                  The behavior of reading from dangling pointers is undefined. The program might crash, you might get seemingly random values. You code produces different output for me.

  • Saumitra Kulkarni

    In the overloaded assignment operator instead of this code,

    I used the earlier constructor instead.

    but this leads to undefined behaviour, When I debugged it, after executing the constructor it called the destructor which deleted the m_data and which caused undefined behaviour. I am unable to understand why is the destructor getting called after executing the constructor. Surely I am missing something silly I guess!

    • nascardriver

      Hi Saumitra!

      You're not calling the constructor of the current instance, you're creating a new @IntArray object which is immediately destroyed, because it's never used.

      This is essentially what you're doing.

      What you can to is moving the contents of the constructor into a separate function and call that function from the constructor and your operator=.

      • David

        I had a similar idea: why not call the overloaded assignment operator from the constructor?

        • nascardriver

          * m_length and m_data are uninitialized, this causes undefined behavior.
          * You're not using member initialization lists, but if you did, you'd need to initialize members to 0 in order for @operator= to work properly.

          Alex' solution looks good as it is, just use uniform initialization.

  • Alexxx

    Example at http://en.cppreference.com/w/cpp/utility/initializer_list doesn't provide list assignment and it not fails because of std::vector type that handle allocations. But IntArray, indeed, fails when assignment used and no list assignment provided.

  • DOG_TRAINER

    Wouldn't this cause a memory leak if list.size() == m_length is true because then m_data wouldn't get deallocated before assigned new elements?

  • Luhan

    In this part

    Wouldn't be better if you did this

  • Ali

    Hi. I have a question. Why should we pass the std::initializer_list as const reference? I did it using non_const reference and it produced garbage results in the overloaded assignment operator and if used in constructor, it produces compiler errors.

    • Alex

      Using a const reference lets us accept std::initializer_list l-values and r-values. When we do something like this:

      The std::initializer_list containing the values is an r-value.

  • AMG

    Alex,
    Minor comment. Xcode gave a warning about implicit conversation from "unsigned int" to "int", and "static_cast" resolved the problem.

    Thank you for the best C++ tutorial!

  • Angmar

    Hi Alex.

    How can a user disambiguate an initializer_list from brace initialization? I have been told that if a class has a constructor taking std::initializer_list, it will override others. How can one elegantly avoid/disambiguate distinguish the constructor for the compiler?

    • Alex

      As you've noted, the initializer_list constructor takes precedence if there's a match, even if the other constructors could match. This seems a bit counterintuitive, but that's the way it is. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2100.pdf talks about why this decision was made, if you want to do some really deep reading.

      If you want to ensure the non-initializer_list constructor is called, use direct initialization (parenthesis).

  • 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 all code inside code tags: [code]your code here[/code]