Почему мой компилятор не может понять это преобразование, когда оно существует? - PullRequest
3 голосов
/ 17 января 2020

Похоже, когда я создаю std::initializer_list<B*>, где class B получается из class A и передаю его функции, которая принимает std::initializer_list<A*>, компилятор запутывается. Однако, если я создаю std::initializer_list<B*> на месте с инициализаторами скобок (который, как я предполагаю, создает временный std::initializer_list), он может конвертировать его очень хорошо.

В частности, кажется, что он не может преобразовать std::initializer_list<B*> в std::initializer_list<A*>, даже если некоторое преобразование явно существует, о чем свидетельствует один вызов рабочей функции.

Чем объясняется такое поведение?

#include <initializer_list>
#include <iostream>


class A {
public:
    A() {}
};

class B : public A {
public:
    B() {}
};

using namespace std;

void do_nothing(std::initializer_list<A*> l) {

}

int main() {
    B* b1;
    B* b2;

    std::initializer_list<B*> blist = {b1, b2};


    //error, note: candidate function not viable: no known conversion from 
    //'initializer_list<B *>' to 'initializer_list<A *>' for 1st argument

    //do_nothing(blist);

    //Totally fine, handles conversion.
    do_nothing({b1, b2});

    return 0;
}

Попробуйте это здесь.

edit:

В качестве обходного пути что-то вроде этого std::initializer_list<A*> alist = {b1, b2}; кажется принятым do_nothing(), но мне все еще интересно, как оно себя ведет.

Ответы [ 3 ]

4 голосов
/ 17 января 2020

Причина этого в том, что список инициализаторов здесь

do_nothing({b1, b2});

имеет тип, отличный от

std::initializer_list<B*> blist = {b1, b2};

Так как do_nothing принимает std::initializer_list<A*> список инициализированных фигурных скобок в вашем вызове функции (do_nothing({b1, b2})) используется для создания std::initializer_list<A*> из вашего параметра функции. Это работает, потому что B* неявно конвертируется в A*. Однако std::initializer_list<B*> неявно не преобразуется в std::initializer_list<A*>, поэтому вы получаете эту ошибку компилятора.

Давайте напишем некоторый псевдокод, чтобы продемонстрировать, что происходит. Сначала мы рассмотрим рабочую часть кода:

do_nothing({b1, b2});  // call the function with a braced-init-list

// pseudo code starts here

do_nothing({b1, b2}):                       // we enter the function, here comes our braced-init-list
   std::initializer_list<A*> l {b1, b2};    // this is our function parameter that gets initialized with whatever is in that braced-init-list
   ...                                      // run the actual function body

, а теперь ту, которая не работает:

std::initializer_list<B*> blist = {b1, b2}; // creates an actual initializer_list
do_nothing(blist);                          // call the function with the initializer_list, NOT a braced-init-list

// pseudo code starts here

do_nothing(blist):                      // we enter the function, here comes our initializer_list
   std::initializer_list<A*> l = blist; // now we try to convert an initializer_list<B*> to an initializer_list<A*> which simply isn't possible
   ...                                  // we get a compiler error saying we can't convert between initializer_list<B*> and initializer_list<A*>

Обратите внимание на условия braced-init -list и initializer_list . Хотя они выглядят одинаково, это две совершенно разные вещи.

A braced-init-list - это пара фигурных скобок со значениями между ними, что-то вроде этого:

{ 1, 2, 3, 4 }

или это:

{ 1, 3.14, "different types" }

это специальная конструкция, используемая для инициализации, которая имеет свои правила на языке C ++.

С другой стороны, std::initializer_list это просто тип (на самом деле шаблон, но мы игнорируем этот факт, поскольку это не имеет значения). Из этого типа вы можете создать объект (как вы сделали с blist) и инициализировать этот объект. И поскольку braced-init-list является формой инициализации, мы можем использовать ее в std::initializer_list:

std::initializer_list<int> my_list = { 1, 2, 3, 4 };

Поскольку в C ++ есть специальное правило, которое позволяет нам инициализировать каждую функцию аргумент с компилированным списком фигурных скобок , do_nothing({b1, b2});. Это также работает для нескольких аргументов:

void do_something(std::vector<int> vec, std::tuple<int, std::string, std::string> tup) 
{ 
    // ...
}

do_something({1, 2, 3, 4}, {10, "first", "and 2nd string"});

или вложенной инициализации:

void do_something(std::tuple<std::tuple<int, std::string>, std::tuple<int, int, int>, double> tup) 
{ 
    // ...
}

do_something({{1, "text"}, {2, 3, 4}, 3.14});
3 голосов
/ 17 января 2020

Мы должны различать guish между std::initializer_list<T> объектом и braced-init-list .

Braced-init-list s заключены в фигурные скобки, разделены запятыми и являются конструкцией уровня источника. Их можно найти в разных контекстах, и их элементы не обязательно должны быть одного типа. Например,

std::pair<int, std::string> = {47, "foo"};  // <- braced-init-list

Когда выполняется оператор, который содержит braced-init-list , иногда создается объект std::initializer_list<T> (для некоторых T) из элементов фигурный список инициализации . Это следующие контексты:

  • Когда переменная, объявленная с типом std::initializer_list<T>, имеет braced-init-list в качестве инициализатора; и
  • Когда объект объявляется с типом auto и инициализируется копией из непустого фигурного списка инициализации .

Если std::initializer_list<T> объект не может быть создан из braced-init-list , возникает ошибка компиляции. Например,

std::initializer_list<int> l = {47, "foo"};  // error

В дополнение к описанным выше способам создания объекта std::initializer_list существует еще один дополнительный способ: путем копирования существующего. Это мелкая копия (она просто заставляет новый объект указывать на тот же массив, что и старый). Копия, конечно, не меняет тип.


Теперь вернемся к вашему коду. Когда вы пытаетесь вызвать функцию do_nothing с аргументом blist, вы пытаетесь сделать что-то невозможное, потому что то, что вы предоставили, не является чем-то, что можно использовать для создания std::initializer_list<A*> объекта. Единственный способ создать такой объект - из braced-init-list или из существующего std::initializer_list<A*> объекта.

Однако передача {b2, b1} в качестве аргумента работает просто отлично потому что это braced-init-list . Допускаются неявные преобразования из элементов braced-init-list в требуемый тип элемента.

1 голос
/ 17 января 2020

std::initializer_list является шаблоном.

Обычно в C ++ some_template<T> не имеет отношения к some_template<U>, даже если T относится к U.

В вызове do_something({b1, b2}) компилятор фактически создает std::initializer_list<A*> (после применения списка-инициализации правил).

Вы можете шаблонизировать do_nothing для принятия различных типов:

template<typename T, typename = std::enable_if_t<std::is_convertible<T, A>::value>>
void do_nothing(std::initializer_list<T*> l) {

}

(необязательная часть SFINAE ограничивает T типами, которые можно преобразовать в A)

Более возможные решения см. этот связанный вопрос .

...