Ready to practice?
Sign up to access interactive coding exercises and track your progress.
std::initializer_list
Accept brace-enclosed lists as constructor or function arguments.
std::initializer_list
Consider a fixed array of integers in C++:
int scores[5];
We can initialize this array directly using initializer list syntax:
#include <iostream>
int main()
{
int scores[]{ 95, 87, 92, 78, 88 };
for (auto score : scores)
std::cout << score << ' ';
return 0;
}
Output:
95 87 92 78 88
This syntax also works with dynamically allocated arrays:
#include <iostream>
int main()
{
auto* scores{ new int[5]{ 95, 87, 92, 78, 88 } };
for (int i{ 0 }; i < 5; ++i)
std::cout << scores[i] << ' ';
delete[] scores;
return 0;
}
In the Container Classes lesson, we created container classes. Here's a simplified TemperatureLog class that stores temperature readings:
#include <cassert>
#include <cstddef>
#include <iostream>
class TemperatureLog
{
private:
int m_count{};
double* m_temperatures{};
public:
TemperatureLog() = default;
TemperatureLog(int capacity)
: m_count{ capacity }
, m_temperatures{ new double[static_cast<std::size_t>(capacity)]{} }
{
}
~TemperatureLog()
{
delete[] m_temperatures;
}
double& operator[](int index)
{
assert(index >= 0 && index < m_count);
return m_temperatures[index];
}
int getCount() const { return m_count; }
};
int main()
{
TemperatureLog weekTemps{ 72.5, 75.3, 68.9, 71.2, 73.8 };
for (int i{ 0 }; i < weekTemps.getCount(); ++i)
std::cout << weekTemps[i] << ' ';
return 0;
}
This code won't compile. The TemperatureLog class lacks a constructor that accepts an initializer list. Without it, we must initialize elements individually:
int main()
{
TemperatureLog weekTemps{ 5 };
weekTemps[0] = 72.5;
weekTemps[1] = 75.3;
weekTemps[2] = 68.9;
weekTemps[3] = 71.2;
weekTemps[4] = 73.8;
for (int i{ 0 }; i < weekTemps.getCount(); ++i)
std::cout << weekTemps[i] << ' ';
return 0;
}
This approach is tedious and error-prone.
Class initialization using std::initializer_list
When the compiler encounters an initializer list, it automatically converts it into a std::initializer_list object. By creating a constructor that accepts a std::initializer_list parameter, we enable list initialization for our classes.
The std::initializer_list type lives in the <initializer_list> header.
Important characteristics of std::initializer_list:
You must specify the element type using angle brackets, unless you immediately initialize it. You'll typically see std::initializer_list<int> or std::initializer_list<std::string>, rarely a plain std::initializer_list.
std::initializer_list provides a size() function returning the element count. This helps determine the list length during initialization.
std::initializer_list is conventionally passed by value. Like std::string_view, std::initializer_list acts as a view, meaning copying it doesn't copy the underlying elements.
Let's add an initializer list constructor to TemperatureLog:
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <initializer_list>
#include <iostream>
class TemperatureLog
{
private:
int m_count{};
double* m_temperatures{};
public:
TemperatureLog() = default;
TemperatureLog(int capacity)
: m_count{ capacity }
, m_temperatures{ new double[static_cast<std::size_t>(capacity)]{} }
{
}
TemperatureLog(std::initializer_list<double> list)
: TemperatureLog(static_cast<int>(list.size()))
{
std::copy(list.begin(), list.end(), m_temperatures);
}
~TemperatureLog()
{
delete[] m_temperatures;
}
TemperatureLog(const TemperatureLog&) = delete;
TemperatureLog& operator=(const TemperatureLog&) = delete;
double& operator[](int index)
{
assert(index >= 0 && index < m_count);
return m_temperatures[index];
}
int getCount() const { return m_count; }
};
int main()
{
TemperatureLog weekTemps{ 72.5, 75.3, 68.9, 71.2, 73.8 };
for (int i{ 0 }; i < weekTemps.getCount(); ++i)
std::cout << weekTemps[i] << ' ';
return 0;
}
Output:
72.5 75.3 68.9 71.2 73.8
Success! Let's examine the initializer list constructor:
TemperatureLog(std::initializer_list<double> list)
: TemperatureLog(static_cast<int>(list.size()))
{
std::copy(list.begin(), list.end(), m_temperatures);
}
Line 1: We specify std::initializer_list<double> because TemperatureLog stores doubles. Notice we pass the list by value rather than by const reference. Like std::string_view, std::initializer_list is lightweight, making copying cheaper than indirection.
Line 2: We delegate memory allocation to the existing constructor using a delegating constructor. We pass list.size() to specify the container size. Since size() returns size_t (unsigned), we cast to signed int.
The constructor body copies elements from the initializer list into our container. We use std::copy() from the <algorithm> header, which provides an efficient way to copy elements.
Accessing elements of a std::initializer_list
Sometimes you need to access individual elements before copying them, perhaps for validation or transformation.
Surprisingly, std::initializer_list doesn't support subscript access via operator[]. This omission has been noted repeatedly but never addressed by the standards committee.
However, several workarounds exist:
- Use a range-based for loop to iterate over elements
- Use the
begin()member function to get an iterator, then use array subscripting on the iterator:
TemperatureLog(std::initializer_list<double> list)
: TemperatureLog(static_cast<int>(list.size()))
{
for (std::size_t i{}; i < list.size(); ++i)
{
m_temperatures[i] = list.begin()[i];
}
}
List initialization prefers list constructors over non-list constructors
Non-empty initializer lists always favor matching initializer list constructors over other potentially matching constructors:
TemperatureLog log1(5); // uses TemperatureLog(int), allocates space for 5 readings
TemperatureLog log2{ 5 }; // uses TemperatureLog(std::initializer_list<double>), creates 1 reading with value 5
For log1, direct initialization doesn't consider list constructors. This calls TemperatureLog(int), allocating space for 5 readings.
For log2, list initialization favors list constructors. Both TemperatureLog(int) and TemperatureLog(std::initializer_list<double>) could match, but the list constructor takes precedence. This creates a log with a single reading of value 5.0.
This preference explains why our delegating constructor uses direct initialization:
TemperatureLog(std::initializer_list<double> list)
: TemperatureLog(static_cast<int>(list.size()))
Direct initialization ensures we delegate to TemperatureLog(int). If we used list initialization, the constructor would try to delegate to itself, causing a compile error.
Standard library containers exhibit the same behavior:
std::vector<int> values(5); // 5 value-initialized elements: 0 0 0 0 0
std::vector<int> values{ 5 }; // 1 element with value 5
List initialization favors matching list constructors over matching non-list constructors.
When initializing a container with a list constructor, use brace initialization when your values are element data, and use direct initialization when your arguments aren't element data.
Adding list constructors to an existing class is dangerous
Because list initialization favors list constructors, adding one to an existing class can silently change program behavior.
Consider this program:
#include <initializer_list>
#include <iostream>
class Widget
{
public:
Widget(int, int)
{
std::cout << "Widget(int, int)\n";
}
};
int main()
{
Widget w1{ 10, 20 };
return 0;
}
Output:
Widget(int, int)
Now let's add a list constructor:
#include <initializer_list>
#include <iostream>
class Widget
{
public:
Widget(int, int)
{
std::cout << "Widget(int, int)\n";
}
Widget(std::initializer_list<int>)
{
std::cout << "Widget(std::initializer_list<int>)\n";
}
};
int main()
{
Widget w1{ 10, 20 };
return 0;
}
The same main() function now produces different output:
Widget(std::initializer_list<int>)
Adding a list constructor to an existing class without one may break existing programs.
Class assignment using std::initializer_list
You can also enable list assignment by overloading the assignment operator to accept std::initializer_list. This works analogously to the constructor version:
TemperatureLog& operator=(std::initializer_list<double> list)
{
int newSize{ static_cast<int>(list.size()) };
if (newSize != m_count)
{
delete[] m_temperatures;
m_count = newSize;
m_temperatures = new double[static_cast<std::size_t>(newSize)]{};
}
std::copy(list.begin(), list.end(), m_temperatures);
return *this;
}
If you implement a list constructor, you should provide at least one of these:
- An overloaded list assignment operator
- A proper deep-copying copy assignment operator
- A deleted copy assignment operator
Here's why: consider a class with a list constructor but without these safeguards:
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <initializer_list>
#include <iostream>
class TemperatureLog
{
private:
int m_count{};
double* m_temperatures{};
public:
TemperatureLog() = default;
TemperatureLog(int capacity)
: m_count{ capacity }
, m_temperatures{ new double[static_cast<std::size_t>(capacity)]{} }
{
}
TemperatureLog(std::initializer_list<double> list)
: TemperatureLog(static_cast<int>(list.size()))
{
std::copy(list.begin(), list.end(), m_temperatures);
}
~TemperatureLog()
{
delete[] m_temperatures;
}
double& operator[](int index)
{
assert(index >= 0 && index < m_count);
return m_temperatures[index];
}
int getCount() const { return m_count; }
};
int main()
{
TemperatureLog weekTemps{};
weekTemps = { 72.5, 75.3, 68.9, 71.2, 73.8, 69.7 };
for (int i{ 0 }; i < weekTemps.getCount(); ++i)
std::cout << weekTemps[i] << ' ';
return 0;
}
The compiler can't find an assignment function taking std::initializer_list. It then searches for other assignment functions, finding the implicitly generated copy assignment operator. However, this operator only accepts a TemperatureLog. The compiler uses the list constructor to convert the initializer list into a temporary TemperatureLog, then calls the implicit copy assignment operator with that temporary.
This causes a shallow copy. Both weekTemps.m_temperatures and the temporary's m_temperatures point to the same memory address.
When the assignment statement completes, the temporary TemperatureLog is destroyed, calling its destructor and deleting its m_temperatures. This leaves weekTemps.m_temperatures as a dangling pointer. Accessing weekTemps.m_temperatures for any reason (including when weekTemps goes out of scope) results in undefined behavior.
If you provide list construction, provide list assignment as well.
Summary
std::initializer_list is a type that represents a lightweight view over an initializer list, automatically created by the compiler when braced initialization is used. It provides a size() function to determine element count and is conventionally passed by value since it acts as a view rather than copying underlying elements.
List initialization for classes is enabled by implementing a constructor that accepts std::initializer_list<T> as a parameter. The compiler automatically converts braced initializer lists into std::initializer_list objects, allowing natural syntax like TemperatureLog weekTemps{ 72.5, 75.3, 68.9, 71.2, 73.8 } to work with custom classes.
Accessing elements from a std::initializer_list can be done through range-based for loops or by using the begin() member function with array subscripting syntax like list.begin()[i]. Surprisingly, std::initializer_list doesn't support direct subscript access via operator[].
List initialization preference: Non-empty initializer lists always favor matching initializer list constructors over other potentially matching constructors. This means TemperatureLog log{ 5 } calls the list constructor (creating one element with value 5.0) rather than the int constructor (allocating space for 5 elements), demonstrating the importance of choosing between brace and direct initialization based on your intent.
List assignment should also be provided when implementing list construction, by overloading the assignment operator to accept std::initializer_list. Without proper list assignment or copy assignment, attempting list assignment can result in shallow copies and dangling pointers due to implicit conversion and the default copy assignment operator.
Implementing std::initializer_list constructors and assignment operators enables natural, intuitive syntax for initializing and assigning to custom container classes, though care must be taken when adding these features to existing classes to avoid breaking existing code.
std::initializer_list - Quiz
Test your understanding of the lesson.
Practice Exercises
Add Initializer List Support to Container
Extend the IntArray container class with std::initializer_list constructor and assignment operator, enabling intuitive brace-initialization syntax like IntArray arr{1, 2, 3, 4, 5}.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!