Last Update:
std::initializer_list in C++ 2/2 - Caveats and Improvements
Table of Contents
In this article, you’ll learn why std::initializer_list
has a bad reputation in C++. Is passing values using it 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:
Updated in Sept 2024: Added note about stack overflow and C++26 fixes.
This is the second part of initalizer_list
mini-series. See the first part on Basics and use cases here.
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 ofconst
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>()
};
}
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.
4. Stack space and large lists (fixed in C++26)
Remember that each initializer list requires a const array of objects… consider this code:
auto getImageBytes() {
return std::vector<std::byte> v = {
0xff, 0xaa, 0xce, ... // 2MB of soma image data
};
}
At first glance, this code is safe, all it does is it allocates dynamic memory and puts the content of the file into that memory block.
But…
Because we have this backing array, we end up with the following code:
auto getImageBytes() {
const std::byte bytes[] = {
0xff, 0xaa, 0xce, ... // 2MB of soma image data
};
std::vector<std::byte> v(bytes.begin(), bytes.end());
return v;
}
This temp array is allocated on the stack. For large arrays (like our 2MB of data), this can exceed the stack size limit and cause a stack overflow, crashing your application.
You can fix this code as of C++23 by using your custom backing array:
auto getImageBytes() {
static const std::byte bytes[] = {
0xff, 0xaa, 0xce, ... // 2MB of soma image data
};
return std::vector<std::byte>(bytes.begin(), bytes.end());
}
Now, we have control over the array and we can put it into a static storage and thus it won’t use stack space.
Fortunately, starting from C++26, thanks to P2752R3 “Static storage for braced initializers”, the compiler will handle this situation more gracefully.
The proposal allows compilers to place the temporary array backing an initializer list into static storage automatically when the array is large. This means that large initializer lists won’t cause stack overflows, and you won’t need to manually declare arrays as static.
For more details, you can read Arthur O’Dwyer’s explanation on #embed and the static storage proposal: P1967 `#embed` and D2752 “Static storage for `initializer_list`” are now on Compiler Explorer – Arthur O’Dwyer – Stuff mostly about C++.
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.
(voted into C++26!): 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.
What’s more, we looked at an improvement from C++26 that allows the compiler to put the backing array for the initializer list in static storage space. This improvement helps with avoiding stack overflows and tricky-to-find bugs.
Resources
- initializer_list class - Microsoft Learn
- The cost of
std::initializer_list
- Andrzej Krzemieński
Back to you
- Do you prefer container initialization with
initializer_list
or regularpush_back()
oremplace_back()
? - Do you tend to optimize and reduce temporary copies when possible?
Share your comments below.
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here:
Similar Articles:
- Fun with printing tables with std::format and C++20
- std::initializer_list in C++ 1/2 - Internals and Use Cases
- How to Use Monadic Operations for `std::optional` in C++23
- Five Advanced Initialization Techniques in C++: From reserve() to piecewise_construct and More.
- Understanding Ranges Views and View Adaptors Objects in C++20/C++23