Передача вектора или вектора векторов или вектора ... (вы поняли) функции - PullRequest
4 голосов
/ 05 марта 2020

Я пишу 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 } } } );

Почему это так? Не будет ли это вектором векторов векторов (то есть «вектором 3D»)?

То же самое, когда я передаю ранее инициализированный вектор:

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

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

Оба распечатайте сообщение BAR: Vector of float. Так что теперь мне нужно go:

bar( { { { v } } } );

, чтобы все заработало, и теперь это выглядит как 4D-вектор. Я что-то упустил?

Ответы [ 3 ]

6 голосов
/ 05 марта 2020

Добро пожаловать в брейс-ад. Если у вас есть

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>>, которая будет использоваться для построения 2-го вектора. Любой вариант так же хорош, так что вы получите двусмысленность. Как вы обнаружили, решением является

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 голосов
/ 05 марта 2020

ответ от NathanOliver объясняет причины неоднозначности.

Если вы заинтересованы в различии guish во всех этих случаях, вам следует добавить другие перегрузки:

#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, здесь .

1 голос
/ 10 марта 2020

Это очень интересный случай list initialization.

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

Это случай инициализации списка копий, здесь временный объект создается путем инициализации списка копий, и к нему привязана ссылка const.

Чтобы понять, как работают функции bar(), необходимо понять, как работает инициализация списка, и правила разрешения его перегрузки. Итак, давайте go один за другим

1009 *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, чтобы оптимизировать временный объект и напрямую инициализируйте other переменную, вызвав std::initializer_list конструктор с {0, 1} в качестве аргумента конструктора.

Наконец, последний случай,

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

Компилятор сначала создаст временный std::vector<double>, и это означает, что express логически становится 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>]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...