Вызов перегруженного static_cast неоднозначен

У меня есть код вроде этого

struct B
{
    B() {}
    B(int v) {}
};

struct A
{
    operator int() const { return 1; }
    operator B() const { return B(); }
};

int main()
{
    A a;
    static_cast<B>(a); // Error here
    a.operator B(); // This is OK
    return 0;
}

Он создает такую ошибку компиляции:

main.cpp: In function ‘int main():
main.cpp:16:21: error: call of overloaded ‘B(A&) is ambiguous
     static_cast<B>(a);
                     ^
main.cpp:4:5: note: candidate: B::B(int)
     B(int v) {}
     ^
main.cpp:1:8: note: candidate: constexpr B::B(const B&)
 struct B
        ^
main.cpp:1:8: note: candidate: constexpr B::B(B&&)

Я не спрашиваю, как это исправить. Просто хотите понять, почему компилятор не принимает его? Из моего POV static_cast<B>(a) совпадает с a.operator B() но, похоже, компилятор читает его по-разному.

Обновить:

Такое поведение происходит до С++ 17. С С++ 17 этот код не создает никакой ошибки компиляции.

Ответы

Ответ 1

С++ 14

Согласно N3797 [expr.static.cast], пункт 4:

Выражение e может быть явно преобразовано в тип T используя static_cast формы static_cast<T>(e) если объявление T t(e); хорошо сформирована, для некоторой изобретенной временной переменной t (8.5). Эффект такого явного преобразования такой же, как выполнение объявления и инициализации, а затем использование временной переменной в результате преобразования.

Выражение static_cast<B>(a) выполняет прямую инициализацию временной переменной типа B из инициализатора a. Затем применяется следующая марка от N3797 [dcl.init], параграф 17:

Если инициализация является прямой инициализацией или если она является копией-инициализацией, в которой рассматривается cv-неквалифицированная версия типа источника того же класса, что или производный класс класса назначения, рассматриваются конструкторы. Соответствующие конструкторы перечислены (13.3.1.3), а лучший выбирается с помощью разрешения перегрузки (13.3). Выбранный таким образом конструктор вызывается для инициализации объекта с выражением инициализатора или списком выражений в качестве аргумента (ов). Если конструктор не применяется или разрешение перегрузки неоднозначно, инициализация плохо сформирована.

И "применимые конструкторы" определены N3797 [over.match.ctor]:

Для прямой инициализации функции-кандидаты - это все конструкторы класса инициализируемого объекта.

Таким образом, все три конструктора: B::B(int), B::B(const B&), B::B(B&&) являются кандидатами во время разрешения перегрузки. Затем A->int три соответствующие имплицитные последовательности преобразования: A->int, A->const B&, A->B&&. В результате A->int отличается от двух других, поскольку условия следующей пули из пункта 3 N3797 [over.ics.rank] не выполняются:

Пользовательская последовательность U1 преобразования является лучшей последовательностью преобразования, чем другая пользовательская последовательность U2 преобразования, если они содержат одну и ту же определяемую пользователем функцию преобразования или конструктор или они инициализируют один и тот же класс в инициализации агрегата, и в любом случае вторая стандартная последовательность преобразования от U1 лучше, чем вторая стандартная последовательность преобразования U2.

Кроме того, не применяются специальные правила определения наилучшей жизнеспособной функции в N3797 [over.match.best], поэтому разрешение перегрузки неоднозначно, что вызывает ошибку компилятора.

С++ 17

Вывод выше также имеет место, поэтому это ошибка компилятора (пока). Формулировка пункта 4 [expr.static.cast] изменилась из-за CWG 242, но это не влияет на наш вывод.

Обратите внимание, что гарантированное копирование не применяется здесь, потому что оно не является инициализацией из знака того же типа.

С++ 20 или более поздняя версия

Уже обсуждается вопрос о добавлении гарантированного экземпляра копии в такие случаи с прямой инициализацией, поэтому, возможно, поведение выбора a.operator B() будет законным в будущем.

Ответ 2

Это связано с тем, что правила direct initialization (в частности, при copy elision) немного изменены в С++ 17, а static_cast<B>(a); просто приводит к:

a.operator B();

Как упоминалось в cppreference/direct_initialization:

если инициализатор является выражением prvalue, тип которого является тем же классом, что и T (игнорируя cv-qualification), само инициализационное выражение, а не временное материализованное из него, используется для инициализации целевого объекта: см. copy elision (поскольку C + +17)

И, читая далее в cppreference/copy_elision, говорится, что с С++ 17 компиляторы должны опускать конструкцию copy- и move-, если

В вызове функции, если операнд оператора return является значением prvalue, а возвращаемый тип функции совпадает с типом этого prvalue.

Таким образом, while static_cast<B>(a); pre С++ 17 можно интерпретировать как B(a.operator int()) и a.operator B(), С++ 17 должен выбрать второй вариант, поскольку возвращаемое значение a.operator B() равно prvalue типа B и он может опустить конструкцию copy-/move-.