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.
#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.
#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::arraystd::array<int, 3> a{1, 2, 3};// Variable initializationint i{2};int j{};// Call function with an std::arrayf({1, 2});// Initialize and use std::mapstd::map<int, std::string> myMap;myMap[1] = "one";myMap[2] = "two";myMap[3] = "three";// Print the contents of the mapstd::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.
What happens if we remove the std::initializer_list<float>
constructor?
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 aT 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.