It is very interesting case of list initialization.
bar({1, 2, 3})
bar({{1, 2, 3}})
It is copy list initialization case, here a temporary object is created by copy list initialization and const reference is bound to it.
To understand how bar() functions call work, it requires to understand how list initialization works and overload resolution rules of it. So let's go one by one,
std::vector<double> vec = {0, 1};
This is also list initialization (copy list initialization), std::vector have following constructor,
vector( std::initializer_list<T> init,
const Allocator& alloc = Allocator() );
std::initializer_list<T> constructor have highest preference compare to other constructor in overload resolution and due to this overload resolution select it.
Another case,
std::vector<double> vec = {0, 1};
std::vector<double> other = {vec};
It is different case, now curly braces have one element only and it is exactly the type of other variable (which is std::vector<double>), as per the list initialization overload resolution rules, more details at special rules for overload resolution, following happens,
Overload resolution doesn't select std::initializer_list<T> constructor rather it selects copy constructor, it is Exact Match rank case, read about it in link provided above. Now this case will help to understand what is happening behind the screen.
Let look at following code,
std::vector<double> other = {{0, 1}};
As per the previous discussion, compiler should fist create a temporary std::vector<double> and then move initialize variable other but fortunately compiler doesn't do it, rather it does following,
So rather creating a temporary std::vector<double> and then move intitialize variable other it optimise-out temporary object and directly initialize other variable by calling std::initializer_list constructor with {0, 1} as constructor argument.
Finally the last case,
std::vector<std::vector<double>> nestedVec = {{0, 1}};
Compiler will first create a temporary std::vector<double> and it means above express logically become std::vector<std::vector<double>> nestedVec = {std::vector<double>{0, 1}}; and then overload resolution will select std::initializer_list constructor.
So the learning is , expression {{1, 2, 3}} is capable to intialize std::vector<double> as well as std::vector<std::vector<double>> and creating ambiguity in bar() function call, it also means std::initializer_list with single element should be used carefully.
Now to resolve ambiguity, following can be done,
change function call,
bar({1, 2, 3});
bar({{1, 2, 3}, {}}); //put an empty element.
or use more pair of curly braces as described in question
bar({{{1, 2, 3}}});
or first crate a variable and pass this variable as function argument rather then passing std::initializer_list as function argument.
following code will demo what i have explained,
#include <iostream>
#include <vector>
using std::cout;
template <class T>
class Container{
public:
Container(){
cout<< "Default contructor.\n"<< __PRETTY_FUNCTION__<< '\n';
}
Container(const Container& ){
cout<< "Copy contructor.\n"<< __PRETTY_FUNCTION__<< '\n';
}
Container(Container&& ){
cout<< "Move contructor.\n"<< __PRETTY_FUNCTION__<< '\n';
}
Container(const std::initializer_list<T>& ){
cout<< "std::initializer_list contructor.\n"<< __PRETTY_FUNCTION__<< '\n';
}
};
int main(int , char *[]){
std::cout<<"1 --- ";
Container<double> dObj; //1
cout<< '\n';
std::cout<<"2 --- ";
[[maybe_unused]] Container<double> cObj = {dObj}; //2
cout<< '\n';
std::cout<<"3 --- ";
[[maybe_unused]] Container<double> lObj = {{0, 1}}; //3
cout<< '\n';
std::cout<<"4 --- ";
[[maybe_unused]] Container<Container<double>> nObj = {{0, 1}}; //4
cout<< '\n';
}
Output:
1 --- Default contructor.
Container<T>::Container() [with T = double]
2 --- Copy contructor.
Container<T>::Container(const Container<T>&) [with T = double]
3 --- std::initializer_list contructor.
Container<T>::Container(const std::initializer_list<_Tp>&) [with T = double]
4 --- std::initializer_list contructor.
Container<T>::Container(const std::initializer_list<_Tp>&) [with T = double]
std::initializer_list contructor.
Container<T>::Container(const std::initializer_list<_Tp>&) [with T = Container<double>]