С++ 17: функция явного преобразования против явного конструктора + неявные преобразования - правила изменились?

Clang 6, clang 7 и gcc 7.1, 7.2 и 7.3 все согласны с тем, что следующее является допустимым кодом С++ 17, но неоднозначно в С++ 14 и С++ 11. MSVC 2015 и 2017 также принимают это. Однако gcc-8.1 и 8.2 отклоняют его даже в режиме С++ 17:

struct Foo
{
    explicit Foo(int ptr);
};

template<class T>
struct Bar
{
    operator T() const;
    template<typename T2>
    explicit operator T2() const;
};


Foo foo(Bar<char> x)
{
    return (Foo)x;
}

Компиляторы, которые принимают его, выбирают шаблонную явную функцию преобразования Bar::operator T2().

Компиляторы, которые отвергают это, соглашаются, что между:

  1. функция явного преобразования Bar :: operator int()
  2. сначала используется неявное пользовательское преобразование из Bar<char> в char, затем неявное встроенное преобразование из char в int, а затем явный конструктор Foo (int).

Итак, какой компилятор прав? В чем разница между стандартом С++ 14 и С++ 17?


Приложение: актуальные сообщения об ошибках

Здесь ошибка для gcc-8.2 -std=c++17. gcc-7.2 -std=c++14 печатает ту же ошибку:

<source>: In function 'Foo foo(Bar<char>)':    
<source>:17:17: error: call of overloaded 'Foo(Bar<char>&)' is ambiguous    
     return (Foo)x;    
                 ^    
<source>:3:14: note: candidate: 'Foo::Foo(int)'    
     explicit Foo(int ptr);    
              ^~~    
<source>:1:8: note: candidate: 'constexpr Foo::Foo(const Foo&)'    
 struct Foo    
        ^~~    
<source>:1:8: note: candidate: 'constexpr Foo::Foo(Foo&&)'

И здесь ошибка от clang-7 -std=c++14 (clang-7 -std=c++17 принимает код):

<source>:17:12: error: ambiguous conversion for C-style cast from 'Bar<char>' to 'Foo'    
    return (Foo)x;    
           ^~~~~~    
<source>:1:8: note: candidate constructor (the implicit move constructor)    
struct Foo    
       ^    
<source>:1:8: note: candidate constructor (the implicit copy constructor)    
<source>:3:14: note: candidate constructor    
    explicit Foo(int ptr);    
             ^    
1 error generated.

Ответ 1

Здесь действуют несколько сил. Чтобы понять, что происходит, давайте рассмотрим, куда (Foo)x должен нас привести. Прежде всего, это приведение в стиле c эквивалентно static_cast в данном конкретном случае. И семантика статического приведения будет заключаться в прямой инициализации объекта результата. Поскольку результирующий объект будет иметь тип класса, [dcl.init]/17.6.2 сообщает нам, что он инициализирован следующим образом:

В противном случае, если инициализация является прямой инициализацией, или если это инициализация копирования, где cv-неквалифицированная версия исходного типа является тем же классом, или производным классом, класса назначения, рассматриваются конструкторы. Применимые конструкторы перечисляются ([over.match.ctor]), и лучший выбирается через разрешение перегрузки. Выбранный таким образом конструктор вызывается для инициализации объекта с выражением инициализатора или списком выражений в качестве аргументов. Если конструктор не применяется, или разрешение перегрузки неоднозначно, инициализация некорректна.

Так что перегрузите разрешение, чтобы выбрать конструктор Foo для вызова. А если не удается разрешить перегрузку, программа работает некорректно. В этом случае он не должен потерпеть неудачу, даже если у нас есть 3 конструктора-кандидата. Это Foo(int), Foo(Foo const&) и Foo(Foo&&).

Во-первых, нам нужно скопировать инициализировать int как аргумент в конструктор, а это значит найти неявную последовательность преобразования из Bar<char> в int. Поскольку определяемый пользователем оператор преобразования, указанный вами из Bar<char> в char, не является явным, мы можем использовать его для неявной последовательности разговоров Bar<char> → char → int.

Для двух других конструкторов нам нужно привязать ссылку к Foo. Однако мы не можем этого сделать. Согласно [over.match.ref]/1:

При условиях, указанных в [dcl.init.ref], ссылка может быть напрямую связана с glvalue или классом prvalue, который является результатом применения функции преобразования к выражению инициализатора. Разрешение перегрузки используется для выбора функции преобразования, которая будет вызвана. Предполагая, что "cv1 T" является базовым типом инициализируемой ссылки, а "cv S" является типом выражения инициализатора, с S типом класса, функции-кандидаты выбираются следующим образом:

  • Рассматриваются функции преобразования S и его базовые классы. Те неявные функции преобразования, которые не скрыты в S и дают тип "lvalue ссылка на cv2 T2" (при инициализации lvalue ссылка или rvalue ссылка на функцию) или "cv2 T2" или "rvalue ссылка на cv2 T2" (когда инициализация ссылки rvalue или ссылки lvalue на функцию), где "cv1 T" является совместимым со ссылкой ([dcl.init.ref]) с "cv2 T2", являются функциями-кандидатами. Для прямой инициализации те явные функции преобразования, которые не скрыты в S и дают тип "lvalue ссылка на cv2 T2" или "cv2 T2" или "rvalue ссылка на cv2 T2", соответственно, где T2 - тот же тип, что и T или могут быть преобразованы в тип T с преобразованием квалификации ([conv.qual]), также являются функциями-кандидатами.

Единственная функция преобразования, которая может дать нам glvalue или prvalue типа Foo - это специализация указанного вами явного шаблона функции преобразования. Но, поскольку инициализация аргументов функции не является прямой инициализацией, мы не можем рассмотреть явную функцию преобразования. Поэтому мы не можем вызывать копирование или перемещать конструкторы в разрешении перегрузки. Это оставляет нас только с конструктором, принимающим int. Таким образом, разрешение перегрузки является успешным, и это должно быть так.

Тогда почему некоторые компиляторы считают это неоднозначным или вместо этого вызывают оператор шаблонного преобразования? Итак, поскольку в стандарт было введено гарантированное разрешение копирования, было отмечено (CWG выпуск 2327), что определяемые пользователем функции преобразования также должны способствовать разрешению копирования. Сегодня, согласно сухому письму стандарта, они этого не делают. Но мы бы очень хотели, чтобы они. Хотя формулировка того, как именно это должно быть сделано, все еще разрабатывается, может показаться, что некоторые компиляторы уже пытаются это реализовать.

И это та реализация, которую вы видите. Это противодействующая сила расширения копий, которая препятствует разрешению перегрузки здесь.