Discussion: An Overloaded Container

Execute the code to understand the output and gain insights into constructor overloading and initialization.

Run the code

Now, it’s time to execute the code and observe the output.

Press + to interact
#include <initializer_list>
#include <iostream>
struct Container
{
Container(int, int)
{
std::cout << "Two ints\n";
}
Container(std::initializer_list<float>)
{
std::cout << "std::initializer_list<float>\n";
}
};
int main()
{
Container container1(1, 2);
Container container2{1, 2};
}

Understanding the output

The Container struct has two constructors, one taking two int types and one taking std::initializer_list<float>. Two objects of type Container are constructed, both with two int types as arguments. But the one difference is that one object is constructed using parentheses—(1,2)—and the other using curly braces—{1,2}. Which constructor is called in each case?

Constructor selection

For Container container1(1, 2), overload resolution unsurprisingly chooses the constructor that takes two int types.

For Container container2{1, 2}, overload resolution chooses the constructor that takes a std::initialization<float>, even though we passed it two int types! Does this mean {1,2} is a std::initializer_list then? No, {1,2} isn’t a std::initializer_list<int> but an expression called a braced-init-list.

The braced-init-list

A braced-init-list is a pair of curly braces containing zero or more elements. It can occur in several contexts; the most common is for initialization.

Examples

Here are some examples of braced-init-lists used for initialization:

std::array<int, 3> a{1, 2, 3};
int i{2};
int j{};
f({1, 2});
std::map<int, std::string>{{1, "one"}, {2, "two"}};
return {1, 2};

As we can see from the initialization of i and j, even if there’s zero or one element and not a list of items between the curly-braces, the expression is still called a braced-init-list.

Press + to interact
#include <iostream>
#include <array>
#include <map>
void f(const std::array<int, 2>& arr)
{
std::cout << "Array contents: ";
for (const auto& elem : arr) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
int main()
{
// Initialize std::array
std::array<int, 3> a{1, 2, 3};
// Variable initialization
int i{2};
int j{};
// Call function with an std::array
f({1, 2});
// Initialize and use std::map
std::map<int, std::string> myMap;
myMap[1] = "one";
myMap[2] = "two";
myMap[3] = "three";
// Print the contents of the map
std::cout << "Map contents:" << std::endl;
for (const auto& [key, value] : myMap) {
std::cout << key << " -> " << value << std::endl;
}
return 0;
}

List-initialization vs. initializer list

Initialization from a braced-init-list is called list-initialization, and the braced-init-list is, unfortunately, called an initializer list in these contexts. An initializer list is not the same as the class template std::initializer_list<T>. The names are rather confusing.

Overload resolution phases

So if {1,2} isn’t a std::initializer_list, why does it pick the std::initializer_list constructor? Overload resolution has a special case for list-initialization where it happens in two phases:

  • First, it does overload resolution for all constructors, taking a std::initializer_list, treating the braced-init-list as one single argument.

  • Then, and only if the first phase didn’t succeed, does it overload resolution for the other constructors, treating the elements of the braced-init-list as separate arguments.

So, in the case of Container container2{1, 2}, overload resolution first tries the std::initializer_list<float> constructor. An int can be implicitly converted to a float, and similarly, an initializer list of int can be used to create a std::initializer_list<float>. So the std::initializer_list<float> constructor is chosen, and the other constructor is never even considered.

Question

What happens if we remove the std::initializer_list<float> constructor?

Show Answer

The std::vector

If you have ever wondered why std::vector<int> v(1,2) gives us a vector with a single element, 2, but std::vector<int> v{1,2} gives us a vector with two elements, 1 and 2, now you know why.

  • The first case is not list-initialization, and it picks the constructor that takes a size_type count and a T value.

  • The second case is list-initialization, and it picks the constructor that takes a std::initializer_list<T>.

Level up your interview prep. Join Educative to access 70+ hands-on prep courses.