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});
}

Run @Compiler Explorer

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:

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 regular push_back() or emplace_back()?

Share your comments below.