Last Update:
std::initializer_list in C++ 1/2 - Internals and Use Cases
Table of Contents
In C++11, we got a handy way to initialize various containers. Rather than using push_back()
or insert()
several times, you can leverage a single constructor by taking an initializer list. For example, with a vector of strings, you can write:
std::vector<std::string> vec { "abc", "xyz", "***" };
We can also write expressions like:
for (auto x : {1, 2, 3}) cout << x << ", ";
The above code samples use std::initializer_list
and (some compiler support) to hold the values and pass them around.
Let’s understand how it works and what are its common uses.
This is the first part of initalizer_list
mini-series. See the second part on Caveats and Improvements here.
Intro to the std::initializer_list
std::initializer_list<T>
, is a lightweight proxy object that provides access to an array of objects of type const T
.
The Standard shows the following example decl.init.list:
struct X {
X(std::initializer_list<double> v);
};
X x{ 1,2,3 };
The initialization will be implemented in a way roughly equivalent to this:
const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));
In other words, the compiler creates an array of const
objects and then passes you a proxy that looks like a regular C++ container with iterators, begin()
, end()
, and even the size()
function. Here’s a basic example that illustrates the usage of this type:
#include <iostream>
#include <initializer_list>
void foo(std::initializer_list<int> list) {
if (!std::empty(list)) {
for (const auto& x : list)
std::cout << x << ", ";
std::cout << "(" << list.size() << " elements)\n";
}
else
std::cout << "empty list\n";
}
int main() {
foo({});
foo({1, 2, 3});
foo({1, 2, 3, 4, 5});
}
The output:
empty list
1, 2, 3, (3 elements)
1, 2, 3, 4, 5, (5 elements)
In the example, there’s a function taking a std::initializer_list
of integers. Since it looks like a regular container, we can use non-member functions like std::empty
, use it in a range-based for loop, and check its size()
. Please notice that there’s no need to pass const initializer_list<int>&
(a const
reference) as the initializer list is a lightweight object, so passing by value doesn’t copy the referenced elements in the “hidden” array.
We can also reveal how the compiler sees the lists using C++ Insights:
int main()
{
foo(std::initializer_list<int>{});
const int __list52[3]{1, 2, 3};
foo(std::initializer_list<int>{__list52, 3});
const int __list134[5]{1, 2, 3, 4, 5};
foo(std::initializer_list<int>{__list134, 5});
return 0;
}
This time we have three separate arrays.
Note that we cannot do the same with std::array
as the parameter to a function would have to have a fixed size. initializer_list
has a variable length; the compiler takes care of that. Moreover, the “internal” array is created on the stack, so it doesn’t require any additional memory allocation (like if you used std::vector
).
The list also takes homogenous values, and the initialization disallows narrowing conversions. For example:
// foo({1, 2, 3, 4, 5.5}); // error, narrowing
foo({1, 'x', '0', 10}); // fine, char converted to int
The text is based on my newest book: C++ initialization story. Check it out @Leanpub.
There’s also a handy use case where you can use range-based for loop directly with the initializer_list
:
#include <iostream>
int main() {
for (auto x : {"hello", "coding", "world"})
std::cout << x << ", ";
}
We can use the magic of C++ Insights and expand the code to see the full compiler transformation, see here:
#include <iostream>
int main()
{
{
const char *const __list21[3]{"hello", "coding", "world"};
std::initializer_list<const char *> && __range1
= std::initializer_list<const char *>{__list21, 3};
const char *const * __begin1 = __range1.begin();
const char *const * __end1 = __range1.end();
for(; __begin1 != __end1; ++__begin1) {
const char * x = *__begin1;
std::operator<<(std::operator<<(std::cout, x), ", ");
}
}
return 0;
}
First, the compiler creates an array to hold string literals, then the __range
, which is an initializer_list
, and then uses a regular range-based for loop.
Let’s now have a look at some use cases for this type.
Use cases
From my observations, there are four primary use cases for initializer_list
:
- creating container-like objects
- implementing custom container-like objects
- utilities like printing/logging
- test code
As usual, I tried to get your help and see your ideas:
do you use std::initializer_list? #cpp
— Bartlomiej Filipek (@fenbf) January 29, 2023
(pick some options, quite limited answers... I know :))
Let’s have a closer look at some examples.
Creating containers
All container classes from the Standard Library are equipped with the support for initializer_list
:
// vector
constexpr vector( std::initializer_list<T> init,
const Allocator& alloc = Allocator() );
// map:
map( std::initializer_list<value_type> init,
const Allocator& );
// ...
And that’s why we can create new objects very easily:
std::vector<int> nums { 1, 2, 3, 4, 5 };
std::map<int, std::string> mapping {
{ 1, "one"}, {2, "two"}, {3, "three"}
};
std::unordered_set<std::string> names {"smith", "novak", "doe" };
While the syntax is convenient, some extra temporary copies might be created. We’ll tackle this issue in the next article.
Array 2D
Standard containers are not special; you can also implement such containers on your own. For example, I’ve found this example for Array2d in TensorFlow repository:
// For example, {{1, 2, 3}, {4, 5, 6}} results in an array with n1=2 and n2=3.
Array2D(std::initializer_list<std::initializer_list<T>> values)
: Array<T>(values) {}
Aliases
And another example where an object can take several values through initializer_list
, in the Krom project:
void AddAlias(const char* from, const char* to);
void AddAlias(const char* from, const std::vector<std::string>& to);
void AddAlias(const char* from, const std::initializer_list<std::string>& to);
Note that initializer_list
will take precedence over std::vector overload. We can show this with the following example @Compiler Explorer:
#include <iostream>
#include <initializer_list>
#include <vector>
void foo(std::initializer_list<int> list) {
std::cout << "list...\n";
}
void foo(const std::vector<int>& list) {
std::cout << "vector...\n";
}
int main() {
foo({});
foo({1, 2, 3});
foo({1, 2, 3, 4, 5});
std::vector<int> temp { 1, 2, 3};
foo(temp);
foo(std::vector { 2, 3, 4});
}
The output:
list...
list...
list...
vector...
vector...
As you can see, passing {...}
will select the initializer_list
version unless we give a “real” vector or construct it explicitly.
A Tricky case
I also found some code in the Libre Office repository:
// ...
static const std::initializer_list<OUStringLiteral> vExtensions
= { "gif", "jpg", "png", "svg" };
OUString aMediaDir = FindMediaDir(rDocumentBaseURL, rFilterData);
for (const auto& rExtension : vExtensions)
// ...
vExtensions
looks like a static collection of string literals. But only the initializer_list
will be static, not the objects themselves!
For example, this:
foo() {
static const std::initializer_list<int> ll { 1, 2, 3 };
}
Expands to:
foo() {
const int __list15[3]{1, 2, 3};
static const std::initializer_list<int> ll = std::initializer_list<int>{__list15, 3};
}
In that case, it’s best to use simple std::array
as the compiler can deduce the number of objects without any issues:
static const std::array nums { 1, 2, 3 };
But maybe that’s more on the “caveats” side…
Summary
In this text, we covered some basics and internals of std::initializer_list
.
As we saw, this wrapper type is only a thin proxy for a compiler-created array of const objects. We can pass it around, iterate with a range-based for loop, or treat it like a “view” container type.
In the following article, we’ll look at some caveats for this type and seek some improvements. Stay tuned :)
Back to you
- Do you write constructors or functions taking
initializer_list
? - Do you prefer container initialization with
initializer_list
or regularpush_back()
oremplace_back()
?
Share your comments below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: