将向量或向量的向量或向量的 ...(你懂的)传递给函数

4
我正在编写一个与机器学习相关的API,我需要有一个重载函数,可以接收一个向量作为参数,或者一个向量的向量(用于批处理)。
但是我在调用该函数时遇到了一些问题。
举个更简单的例子,该函数可能如下所示:
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;
}

因此,我希望可以这样调用它:

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

但是在第二个函数中,IDE抱怨说,两个重载函数都匹配参数列表,所以我必须像这样调用它才能使其工作。

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

为什么呢?那不是一种向量的向量的向量(即“三维向量”)吗?

同样的情况出现在我传递一个之前初始化过的向量时:

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

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

两者都输出了 BAR: Vector of float 的信息。现在我需要进行以下操作:

bar( { { { v } } } );

为了让它起作用,现在看起来像一个4D向量。我是否遗漏了什么?

“我有什么遗漏吗?”是的,当你超越简单、最常见的例子时,“initializer_list”和“list-init”确实很奇怪。 - bolov
3个回答

6

欢迎来到花括号地狱。当你

bar( { 1,2,3 } );

{ 1,2,3 }被视为一个std::initializer_list<float>,可调用的唯一函数是void bar(const std::vector<float>& arg)

当您有

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

现在可以将{{1,2,3}}解释为外部大括号表示std::vector<float>,而内部大括号表示其std::initializer_list<float>,或者是构建std::initializer_list<std::initializer_list<float>>以用于构建二维向量。两种选择都一样好,因此存在歧义。正如你所发现的那样,解决方案是

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

现在,最外层的一对大括号表示创建一个std::vector<std::vector<float>>,第二层大括号表示开始一个std::initializer_list<std::initializer_list<float>>,而最内层的大括号表示其中的一个元素。

通过

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

这有点复杂。显然,bar(v);会做你想要的事情,但是bar({v});实际上是有效的,不像bar({{1,2,3}});,因为在[over.ics.list]中存在的规则。具体而言,第7段{v}通过复制构造函数创建std::vector<float>是一个精确匹配,而创建std::vector<std::vector<float>>则是用户定义转换。这意味着调用void bar(const std::vector<float>& arg)更加匹配,这就是你看到的结果。你需要使用:
bar( { { { v } } } );

这样,最外层的大括号表示 std::vector<std::vector<float>>,中间的大括号表示 std::initializer_list<std::vector<float>> 的开始,而最内层的大括号则是该列表中的单个 std::vector<float> 元素。


3
这篇答案的作者是NathanOliver,他解释了歧义产生的原因。

如果你想要区分所有这些情况,你应该添加其他重载函数:

#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
}

现场,请点击这里查看。


好的,这听起来是个不错的主意,我明天会试一下。我可以为那些做着基本相同事情的函数使用更少的不同名称。 - xonxt

1
这是一个非常有趣的列表初始化案例。
bar({1, 2, 3})
bar({{1, 2, 3}})

这是一个复制列表初始化的案例,通过复制列表初始化创建了一个临时对象,并将const引用绑定到它上面。
要理解bar()函数调用的工作原理,需要了解列表初始化的工作方式以及其重载解析规则。因此,让我们逐一进行解释。 std::vector<double> vec = {0, 1}; 这也是列表初始化(复制列表初始化),std::vector有以下构造函数。
vector( std::initializer_list<T> init,
        const Allocator& alloc = Allocator() ); 

"

std::initializer_list<T>构造函数在重载决议中比其他构造函数优先级更高,因此重载决议会选择它。

另一种情况,

"
std::vector<double> vec = {0, 1};
std::vector<double> other = {vec};

这是不同的情况,现在花括号只有一个元素,且正好是other变量的类型(即std::vector<double>),根据列表初始化重载解析规则,更多细节请参考重载解析的特殊规则,以下发生了以下情况:

重载解析未选择std::initializer_list<T>构造函数,而是选择复制构造函数,这是完全匹配等级的情况,请阅读上面提供的链接了解详细信息。现在这个案例将有助于理解幕后发生了什么。

让我们看一下以下代码:

std::vector<double> other = {{0, 1}};

根据先前的讨论,编译器应该首先创建一个临时的std::vector<double>,然后再使用移动初始化变量other,但幸运的是编译器并没有这样做,而是进行了优化,

因此,编译器不是创建一个临时的std::vector<double>,然后再移动初始化变量other,而是优化掉了临时对象,并通过调用std::initializer_list构造函数直接使用{0, 1}作为构造参数来直接初始化other变量。

最后是最后一种情况,

std::vector<std::vector<double>> nestedVec = {{0, 1}};

编译器将首先创建一个临时的std::vector<double>,这意味着上述表达式在逻辑上变成了std::vector<std::vector<double>> nestedVec = {std::vector<double>{0, 1}};,然后重载决议将选择std::initializer_list构造函数。

所以学习的内容是,表达式{{1, 2, 3}}能够初始化std::vector<double>以及std::vector<std::vector<double>>,并在bar()函数调用中创建歧义,这也意味着应该谨慎使用带有单个元素的std::initializer_list

为了消除歧义,可以采取以下措施:
更改函数调用,
bar({1, 2, 3});
bar({{1, 2, 3}, {}}); //put an empty element.

或者按照问题描述使用更多的花括号对

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

或者首先创建一个变量,并将此变量作为函数参数传递,而不是将std::initializer_list作为函数参数传递。

以下代码将演示我所解释的内容,

#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';
}

输出:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接