Passing a vector or a vector of vectors or a vector of ... (you get the idea) to a function

Viewed 109

I'm writing an API for a Machine Learning, and I need to have an overloaded function that can either receive a vector as an argument, or a vector of vectors (for a batch job).

I'm a having a bit of a problem with calling the function though.

As an simpler example, the function might look like this:

void bar( const std::vector<float>& arg ) {
  std::cout << "BAR: Vector of float" << std::endl;
}
void bar( const std::vector<std::vector<float>>& arg ) {
  std::cout << "BAR: Vector of vectors of float" << std::endl;
}

So, I'd expect that I could call it like this:

 bar( { 1,2,3 } );
 bar( { { 1,2,3 } } );

But on the second one the IDE complains that, BOTH overloaded functions match the argument list, and so I have to call it like this for it to work.

bar( { { { 1,2,3 } } } );

Why is that? Wouldn't that be a vector of vectors of vectors (i.e. a "3D-vector")?

The same is when I'm passing a previously initialized vector:

std::vector<float> v = { 1,2,3,4,5 };

bar( v );
bar( { v } );

Both print out the BAR: Vector of float message. So I now have to go:

bar( { { { v } } } );

for it to work, and this now looks like a 4D-vector. Am I missing something?

3 Answers

Welcome to brace hell. When you have

bar( { 1,2,3 } );

the { 1,2,3 } is treated as a std::initializer_list<float> and the only viable function to call is void bar( const std::vector<float>& arg )

When you have

bar( { { 1,2,3 } } );

Now { { 1,2,3 } } can be construed as the outer braces denoting a std::vector<float>, and the inner braces the std::initializer_list<float> for it, or could be the construction of a std::initializer_list<std::initializer_list<float>> to be used to construct the 2d vector. Either option is just as good, so you get an ambiguity. As yo found, the solution is

bar( { { { 1,2,3 } } } );

So now the outer most set of braces denotes creating a std::vector<std::vector<float>> and the second outer most braces denote the start of a std::initializer_list<std::initializer_list<float>> and the inmost braces being an element of that.

With

bar( v );
bar( { v } );

It is a little more complicated. Obviously bar( v ); will do what you want, but bar( { v } ); actually works unlike bar( { { 1,2,3 } } ); because of the rules in [over.ics.list]. Specifically, paragraph 7 says that { v } is an exact match for creating a std::vector<float> via the copy constructor while creating a std::vector<std::vector<float>> is a user defined conversion. This means that calling void bar( const std::vector<float>& arg ) is a better match and that is what you see. You need to use

bar( { { { v } } } );

so that the outer set of braces denotes the std::vector<std::vector<float>> and the middle set is the start of the std::initializer_list<std::vector<float>>, and the innermost set is the single std::vector<float> element of that list.

The answer by NathanOliver explains the reasons of the ambiguity.

If you are interested in distinguish all those cases, you should add other overloads:

#include <initializer_list>
#include <iostream>
#include <vector>

void bar( std::vector<float> const& arg ) {
  std::cout << "BAR: Vector of float, size " << arg.size() << '\n';
}

void bar( std::vector<std::vector<float>> const& arg ) {
  std::cout << "BAR: Vector of vectors of float, size " << arg.size() << '\n';
}

void bar( std::initializer_list<float> lst ) {
  std::cout << "BAR: Initializer list of float, size " << lst.size() << '\n';
}

void bar( std::initializer_list<std::initializer_list<float>> lst ) {
  std::cout << "BAR: Initializer list of initializer list of float, size "
            << lst.size() << '\n';
}

void bar( std::initializer_list<std::vector<float>> lst ) {
  std::cout << "BAR: Initializer list of vector of float, size "
            << lst.size() << '\n';
}

int main()
{
    bar( { 1,2,3 } );     // -> Initializer list of float
    bar( { { 1,2,3 } } ); // -> Initializer list of initializer list of float

    std::vector<float> v = { 1,2,3,4,5 };

    bar( v );     // -> Vector of float
    bar( { v } ); // -> Initializer list of vector of float
}

Live, here.

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>]
Related