簡體   English   中英

將向量或向量的向量或...的向量(你懂的)傳遞給函數

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

我正在為機器學習編寫 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 中,IDE 抱怨說,兩個重載函數都匹配參數列表,因此我必須像這樣調用它才能使其工作。

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

這是為什么? 那不是向量的向量(即“3D-向量”)嗎?

當我傳遞一個先前初始化的向量時也是如此:

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

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

兩者都打印出BAR: Vector of float消息。 所以我現在必須去:

bar( { { { v } } } );

讓它工作,這現在看起來像一個 4D 向量。 我錯過了什么嗎?

歡迎來到地獄。 當你有

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>元素。

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
}

在這里

這是一個非常有趣的list initialization案例。

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

這是復制列表初始化的情況,這里通過復制列表初始化創建了一個臨時對象,並綁定了常量引用。

要了解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> ),根據list initialization重載解析規則,更多細節在重載解析的特殊規則中,發生以下情況,

重載解析不選擇std::initializer_list<T>構造函數,而是選擇復制構造函數,它是完全匹配等級的情況,請在上面提供的鏈接中閱讀。 現在這個案例將有助於了解屏幕后面發生的事情。

讓我們看看下面的代碼,

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

根據前面的討論,編譯器應該先創建一個臨時的std::vector<double> ,然后移動初始化變量other但幸運的是編譯器沒有這樣做,而是跟隨,

因此,而不是創建一個臨時std::vector<double>然后移動初始化變量other它優化臨時對象並通過調用std::initializer_list構造函數直接初始化other變量,並使用{0, 1}作為構造函數參數。

最后一個案例,

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM