Table of Contents

In this article, you’ll learn why std::initializer_list has a bad reputation in C++. Is passing values using is as efficient as “emplace”, how can we use non-copyable types? You’ll also see how to fix some of the problems.

Let’s start with the issues first:

1. Referencing local array  

If you recall from the previous article, std::initializer_list expands to some unnamed local array of const objects. So the following code might be super unsafe:

std::initializer_list<int> wrong() { // for illustration only!
    return { 1, 2, 3, 4};
}
int main() {
    std::initializer_list<int> x = wrong();
}

The above code is equivalent to the following:

std::initializer_list<int> wrong() {
    const int arr[] { 1, 2, 3, 4}
    return std::initializer_list<int>{arr, arr+4};
}
int main() {
    std::initializer_list<int> x = wrong();
}

The example serves to highlight the error, emphasizing the importance of avoiding similar mistakes in our own code. The function returns pointers/iterators to a local object, and that will cause undefined behavior. See a demo @Compiler Explorer.

GCC or Clang reports a handy warning in this case:

GCC:
warning: returning temporary 'initializer_list' does not extend the lifetime of the underlying array [-Winit-list-lifetime]
    5 |     return { 1, 2, 3, 4};

Or in Clang:

<source>:5:12: warning: returning address of local temporary object [-Wreturn-stack-address]
    return { 1, 2, 3, 4};

We can make the following conclusion:

std::initializer_list is a “view” type; it references some implementation-dependent and a local array of const values. Use it mainly for passing into functions when you need a variable number of arguments of the same type. If you try to return such lists and pass them around, then you risk lifetime issues. Use with care.

Let’s take on another limitation:

2. The cost of copying elements  

Passing elements through std::initializer_list is very convenient, but it’s good to know that when you pass it to a std::vector’s constructor (or other standard containers), each element has to be copied. It’s because, conceptually, objects in the initializer_list are put into a const temporary array, so they have to be copied to the container.

Let’s compare the following case:

std::cout << "vec... { }\n";
std::vector<Object> vec { Object("John"), Object("Doe"), Object("Jane") };

With:

std::cout << "vec emplace{}\n";
std::vector<Object> vec;
vec.reserve(3);
vec.emplace_back("John");
vec.emplace_back("Doe");
vec.emplace_back("Jane");

The Object class is a simple wrapper around std::string with some extra logging code. You can run the example @Compiler Explorer.

And we’ll get:

vec... { }
MyType::MyType John
MyType::MyType Doe
MyType::MyType Jane
MyType::MyType(const MyType&) John
MyType::MyType(const MyType&) Doe
MyType::MyType(const MyType&) Jane
MyType::~MyType Jane
MyType::~MyType Doe
MyType::~MyType John
MyType::~MyType John
MyType::~MyType Doe
MyType::~MyType Jane
vec emplace{}
MyType::MyType John
MyType::MyType Doe
MyType::MyType Jane
MyType::~MyType John
MyType::~MyType Doe
MyType::~MyType Jane

As you can see, something is wrong! In the case of the single constructor, we can spot some extra temporary objects! On the other hand, the case with emplace_back() has no temporaries.

We can link this issue with the following element:

3. Non-copyable types  

The extra copy that we’d get with the initializer_list, also causes issues when your objects are not copyable. For example, when you want to create a vector of unique_ptr.

#include <vector>
#include <memory>

struct Shape {    virtual void render() const ; };
struct Circle : Shape { void render() const override; };
struct Rectangle : Shape {  void render() const override; };

int main() {
    std::vector<std::unique_ptr<Shape>> shapes {
        std::make_unique<Circle>(), std::make_unique<Rectangle>()
    };
}

Run @Compiler Explorer

The line where I want to create a vector fails to compile, and we get many messages about copying issues.

In short: the unique pointers cannot be copied. They can only be moved, and passing initializer_list doesn’t give us any options to handle those cases. The only way to build such a container is to use emplace_back or push_back:

std::vector<std::unique_ptr<Shape>> shapes;
shapes.reserve(2);
shapes.push_back(std::make_unique<Circle>());           // or
shapes.emplace_back(std::make_unique<Rectangle>());

See the working code at Compiler Explorer.

Alternative: variadic function  

If you want to reduce the number of temporary objects, or your types are not copyable, then you can use the following factory function for std::vector:

template<typename T, typename... Args>
auto initHelper(Args&&... args) {
    std::vector<T> vec;
    vec.reserve(sizeof...(Args)); 
    (vec.emplace_back(std::forward<Args>(args)), ...);
    return vec;
}

See at @Compiler Explorer

When we run the code:

std::cout << "initHelper { }\n";
auto vec = initHelper<Object>("John", "Doe", "Jane");

We’ll get the following:

initHelper { }
MyType::MyType John
MyType::MyType Doe
MyType::MyType Jane
MyType::~MyType John
MyType::~MyType Doe
MyType::~MyType Jane

What’s more, the same function template can be used to initialize from moveable objects:

std::vector<std::unique_ptr<Shape>> shapes;
shapes.reserve(2);
shapes.push_back(std::make_unique<Circle>());
shapes.emplace_back(std::make_unique<Rectangle>());

auto shapes2 = initHelper<std::unique_ptr<Shape>>(
     std::make_unique<Circle>(), std::make_unique<Rectangle>());

Run at @Compiler Explorer

Core features:

  • Variadic templates allow us to pass any number of arguments into a function and process it with argument pack syntax.
  • (vec.emplace_back(std::forward<Args>(args)), ...); is a fold expression over a comma operator that nicely expands the argument pack at compile time. Fold expressions have been available since C++17.

More proposals  

The initializer list is not perfect, and there were several attempts to fix it:

From the paper N4166 by David Krauss

std::initializer_list was designed around 2005 (N1890) to 2007 (N2215), before move semantics matured, around 2009. At the time, it was not anticipated that copy semantics would be insufficient or even suboptimal for common value-like classes. There was a 2008 proposal “N2801 Initializer lists and move semantics” but C++0x was already felt to be slipping at that time, and by 2011 the case had gone cold.

There’s also a proposal by Sy Brand: WG21 Proposals - init list

Still, as of 2022, there have yet to be any improvements implemented in the standard. So we have to wait a bit.

Recently there’s also a proposal from Arthur: P1967 `#embed` and D2752 “Static storage for `initializer_list`” are now on Compiler Explorer. This is very interesting as it allows us to put that unnamed const array into the static storage rather than on the stack. We’ll see how it goes.

Summary  

In the article, I showed you three issues with initializer_list: extra copies, lack of support for non-copyable objects, and undefined behavior when returning such lists. The case with non-copyable objects can be solved by using a relatively simple factory function that accepts a variadic pack.

Resources  

Back to you

  • Do you prefer container initialization with initializer_list or regular push_back() or emplace_back()?
  • Do you tend to optimize and reduce temporary copies when possible?

Share your comments below.