Явные конструкторы и вложенные списки инициализаторов

Следующий код успешно компилируется с большинством современных компиляторов, совместимых с С++ 11 (GCC >= 5.x, Clang, ICC, MSVC).

#include <string>

struct A
{
        explicit A(const char *) {}
        A(std::string) {}
};

struct B
{
        B(A) {}
        B(B &) = delete;
};

int main( void )
{
        B b1({{{"test"}}});
}

Но зачем он компилируется в первую очередь и как перечисленные компиляторы интерпретируют этот код?

Почему MSVC может скомпилировать это без B(B &) = delete;, но все остальные 3 компилятора им нужны?

И почему это происходит во всех компиляторах, кроме MSVC, когда я удаляю различную подпись конструктора копирования, например. B(const B &) = delete;?

Разве компиляторы даже выбирают одни и те же конструкторы?

Почему Clang выдает следующее предупреждение?

17 : <source>:17:16: warning: braces around scalar initializer [-Wbraced-scalar-init]
        B b1({{{"test"}}});

Ответ 1

Вместо объяснения поведения компиляторов я попытаюсь объяснить, что говорит стандарт.

Основной пример

Для прямой инициализации b1 из {{{"test"}}} применяется разрешение перегрузки, чтобы выбрать лучший конструктор B. Поскольку неявное преобразование из {{{"test"}}} в B& (инициализатор списка не является lvalue), конструктор B(B&) не является жизнеспособным. Затем мы фокусируемся на конструкторе B(A) и проверяем его жизнеспособность.

Чтобы определить неявную последовательность преобразований от {{{"test"}}} до A (для простоты я буду использовать обозначение {{{"test"}}}A), разрешение перегрузки применяется для выбора лучшего конструктора A, поэтому мы нужно сравнить {{"test"}}const char* и {{"test"}}std::string (обратите внимание, что самый внешний слой фигурных скобок устранен) в соответствии с [over. match.list]/1:

Когда объекты неагрегатного типа типа T инициализируются по списку, так что [dcl.init.list] указывает, что разрешение перегрузки выполняется в соответствии с правилами в этом подпункте, разрешение перегрузки выбирает конструктор в две фазы:

  • Изначально функции-кандидаты являются конструкторами-конструкторами-инициализаторами ([dcl.init.list]) класса T...

  • Если не найдено жизнеспособного конструктора-списка инициализаторов, снова выполняется разрешение перегрузки, где функции-кандидаты являются всеми конструкторами класса T, а список аргументов состоит из элементов списка инициализаторов.

... В инициализации списка копий, если выбран явный конструктор, инициализация плохо сформирована.

Обратите внимание, что все конструкторы рассматриваются здесь независимо от спецификатора explicit.

{{"test"}}const char* не существует в соответствии с [over.ics.list]/10 и [over.ics.list]/11:

В противном случае, если тип параметра не является классом:

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

  • если в списке инициализаторов нет элементов...

Во всех случаях, отличных от перечисленных выше, преобразование невозможно.

Чтобы определить {{"test"}}std::string, выполняется тот же процесс, и разрешение перегрузки выбирает конструктор std::string, который принимает параметр типа const char*.

В результате {{{"test"}}}A выполняется путем выбора конструктора A(std::string).


Варианты

Что делать, если explicit удален?

Процесс не изменяется. GCC выберет конструктор A(const char*), а Кланг выберет конструктор A(std::string). Я думаю, что это ошибка для GCC.

Что делать, если в инициализаторе b1?

есть только два слоя фигурных скобок,

Примечание {{"test"}}const char* не существует, но существует {"test"}const char*. Поэтому, если в инициализаторе b1 есть только два слоя фигурных скобок, конструктор A(const char*) выбирается потому, что {"test"}const char* лучше, чем {"test"}std::string. В результате явный конструктор выбирается в инициализации списка копий (инициализация параметра A в конструкторе B(A) из {"test"}), тогда программа плохо сформирована.

Что делать, если объявлен конструктор B(const B&)?

Обратите внимание, что это также происходит, если объявление B(B&) удалено. На этот раз нам нужно сравнить {{{"test"}}}A и {{{"test"}}}const B&, или {{{"test"}}}const B эквивалентно.

Чтобы определить {{{"test"}}}const B, описан процесс, описанный выше. Нам нужно сравнить {{"test"}}A и {{"test"}}const B&. Примечание {{"test"}}const B& не существует в соответствии с [over.best.ics]/4:

Однако, если цель

- первый параметр конструктора или

- параметр неявного объекта определяемой пользователем функции преобразования

и конструктор или пользовательская функция преобразования является кандидатом

- [over.match.ctor], когда аргумент является временным на втором этапе инициализации экземпляра класса,

- [over.match.copy], [над .match.conv] или [над .match.ref] (во всех случаях) или

- вторая фаза [over.match.list], когда список инициализаторов имеет ровно один элемент, который сам является списком инициализаторов, а целью является первый параметр конструктора класса X, а преобразование является X или ссылка на cv X,

пользовательские последовательности преобразований не рассматриваются.

Чтобы определить {{"test"}}A, описанный выше процесс снова берется. Это почти то же самое, что и в предыдущем подразделе. В результате выбирается конструктор A(const char*). Обратите внимание, что здесь выбирается конструктор, чтобы определить {{{"test"}}}const B и на самом деле не применяется. Это разрешено, хотя конструктор явно.

В результате {{{"test"}}}const B выполняется путем выбора конструктора B(A), затем конструктора A(const char*). Теперь оба {{{"test"}}}A и {{{"test"}}}const B являются пользовательскими последовательностями преобразования, и ни один не лучше другого, поэтому инициализация b1 неоднозначна.

Что делать, если скобки заменяются скобками?

В соответствии с [over.best.ics]/4, который блокируется в предыдущем подразделе, пользовательское преобразование {{{"test"}}}const B& не рассматривается. Таким образом, результат будет таким же, как и в первом примере, даже если объявлен конструктор B(const B&).

Ответ 2

B b1({{{"test"}}}); выглядит как B b1(A{std::string{const char*[1]{"test"}}});

16.3.3.1.5 Последовательность инициализации списка [over.ics.list]

4 В противном случае, если тип параметра представляет собой массив символов 133, а в списке инициализаций есть один элемент, типизированный строковый литерал (11.6.2), неявная последовательность преобразования - это преобразование идентичности.

И компилятор пытается все возможные неявные преобразования. Например, если у нас есть класс C со следующими конструкторами:

#include <string>

struct C
{
    template<typename T, size_t N>     C(const T* (&&) [N]) {}
    template<typename T, size_t N>     C(const T  (&&) [N]) {}
    template<typename T=char>         C(const T* (&&)) {}
    template<typename T=char>          C(std::initializer_list<char>&&) {}
};

struct A
{
    explicit A(const char *) {}

    A(C ) {}
};

struct B
{
    B(A) {}
    B(B &) = delete;
};

int main( void )
{
    const char* p{"test"};
    const char p2[5]{"test"};

    B b1({{{"test"}}});
}

Компилятор clang 5.0.0 не мог решить, с каким использованием и сбой:

29 : <source>:29:11: error: call to constructor of 'C' is ambiguous
    B b1({{{"test"}}});
          ^~~~~~~~~~
5 : <source>:5:40: note: candidate constructor [with T = char, N = 1]
    template<typename T, size_t N>     C(const T* (&&) [N]) {}
                                       ^
6 : <source>:6:40: note: candidate constructor [with T = const char *, N = 1]
    template<typename T, size_t N>     C(const T  (&&) [N]) {}
                                       ^
7 : <source>:7:39: note: candidate constructor [with T = char]
    template<typename T=char>         C(const T* (&&)) {}
                                      ^
15 : <source>:15:9: note: passing argument to parameter here
    A(C ) {}
        ^

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

GCC 7.2 просто выбирает C(const T* (&&)) {} и компилирует. Если он недоступен, требуется C(const T* (&&) [N]).

MSVC просто терпит неудачу:

29 : <source>(29): error C2664: 'B::B(B &)': cannot convert argument 1 from 'initializer list' to 'A'

Ответ 3

(Отредактировано, спасибо @dyp)

Вот частичный ответ и умозрительный, объясняющий, как я начал интерпретировать происходящее, не будучи экспертом по компиляции и не очень большим количеством С++ Guru.

Сначала я пойду с некоторой интуицией и здравым смыслом. Очевидно, что последнее, что должно произойти, это B::B(A), так как это единственный конструктор, доступный для B b1 (видимо, он не может быть B::B(B&&), потому что существует хотя бы один конструктор копирования, поэтому B::B(B&&) неявно определяется для нас). Кроме того, первая конструкция A или B не может быть A::A(const char*), потому что эта явная, поэтому должно быть некоторое использование A::A(std::string). Кроме того, самый внутренний цитируемый текст - const char[5]. Поэтому я угадаю, что первая, самая внутренняя конструкция - это const char*; а затем построение строки: std::string::string(const char *). Там еще одна фигурная конструкция, и я бы предположил, что она A::A(A&&) (или, может быть, A::A(A&)?). Итак, чтобы обобщить мою интуитивную догадку, порядок конструкций должен быть:

  • A const char*
  • An std::string (что действительно std::basic_string<whatever>)
  • a A
  • a B

Затем я положил это на GodBolt, а GCC - в качестве первого примера. (Кроме того, вы можете просто скомпилировать его самостоятельно, сохраняя вывод ассемблера и передавая это через c++filt, чтобы сделать его более читаемым). Вот все строки, в которых конкретно упоминается код С++:

call   4006a0 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)@plt>
call   400858 <A::A(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)>
call   400868 <B::B(A)>
call   400680 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()@plt>
call   400690 <std::allocator<char>::~allocator()@plt>
call   400690 <std::allocator<char>::~allocator()@plt>

Таким образом, мы видим, что порядок надлежащих управляемых конструкций мы видим:

(не видя 1.) 2. std::basic_string::basic_string(const char* /* ignoring the allocator */) 3. A::A(std::string) 4. B::B(A)

С clang 5.0.0, результаты аналогичны IIANM, а для MSVC - кто знает? Может быть, это ошибка? Они, как известно, немного изворотливы, иногда при правильной поддержке языкового стандарта. Извините, как я уже сказал - частичный ответ.