Clang и GCC не согласны с законностью прямой инициализации с оператором преобразования

Последняя версия clang (3.9) отклоняет этот код во второй строке f; последняя версия gcc (6.2) принимает его:

struct Y {
    Y();
    Y(const Y&);
    Y(Y&&);
};

struct X {
    operator const Y();
};

void f() {
    X x;
    Y y(x);
}

Если какое-либо из этих изменений сделано, clang примет код:

  • Удалить Y move constructor
  • Удалите const из оператора преобразования
  • Замените Y y(x) на Y y = x

Является ли исходный пример законным? Какой компилятор ошибочен? После проверки разделов о функциях преобразования и разрешении перегрузки в стандарте я не смог найти четкий ответ.

Ответ 1

Когда мы перечисляем конструкторы и проверяем их жизнеспособность - то есть, есть ли неявная последовательность преобразований - для конструктора перемещения, [dcl.init.ref ]/5 доходит до последней точки (5.2.2), которая была изменена основными проблемами 1604 и 1571 (в этом порядке).

Нижняя строка этих разрешений состоит в том, что

Если T1 или T2 - тип класса, а T1 не ссылается на T2, пользовательские преобразования рассмотренные с использованием правил для инициализации копии объекта типа "cv1 T1" посредством пользовательского преобразования (8.6, 13.3.1.4, 13.3.1.5); программа будет плохо сформирована, если соответствующая неосновная копия-инициализация будет плохо сформирована. Результат вызова функции преобразования, как описано для инициализации без ссылки, затем используется для прямой инициализации ссылки.

Первая часть просто вызывает выбор оператора преобразования. Итак, согласно смелой части, мы используем const Y для прямой инициализации Y&&. Опять же, мы проваливаемся до последней точки маркера, которая терпит неудачу из-за (5.2.2.3):

Если T1 ссылается на T2:
- cv1 должны быть одинаковыми cv-квалификация, или более высокая cv-квалификация, чем cv2; и

Однако это больше не относится к нашему первоначальному разрешению перегрузки, но только видит, что оператор преобразования используется для прямой инициализации ссылки. В вашем примере разрешение перегрузки выбирает конструктор перемещения, потому что [over.ics.rank]/(3.2.5), а затем вышеприведенный абзац делает программа плохо сформирована. Это дефект и был зарегистрирован как основной вопрос 2077. Разумное решение отбрасывает конструктор перемещения во время разрешения перегрузки.

Все это имеет смысл по отношению к вашим исправлениям: удаление const предотвратит провал, так как типы теперь совместимы со ссылкой, и удаление конструктора перемещения оставляет конструктор копирования, который имеет ссылку на константу (т.е. работает как Что ж). Наконец, когда мы пишем Y y = x;, вместо [dcl.init]/(17.6.2) применяется (17.6.3):

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

т.е. инициализация фактически такая же, как Y y(x.operator const Y());, которая преуспевает, потому что конструктор перемещения не является жизнеспособным (Y&& y = const Y не выполняется достаточно мелко) и выбран конструктор копирования.

Ответ 2

Я думаю, что это ошибка clang.

Начнем с [over.match.ctor]:

Когда объекты типа класса инициализируются с прямой инициализацией (8.6), копирование выполняется из выражения того же или тип производного класса (8.6) или инициализированный по умолчанию (8.6), разрешение перегрузки выбирает конструктор. Для прямой инициализации или инициализация по умолчанию, которая не относится к инициализации копирования, функции-кандидаты все конструкторы класса инициализируемого объекта.

Итак, мы рассматриваем, например, конструктор копирования. Является ли конструктор копирования жизнеспособным?

Из [dcl.init.ref]:

- Если выражение инициализатора [...] имеет тип класса (т.е. T2 - тип класса), где T1 не относится к T2 и может быть преобразуется в rvalue типа "cv3 T3", где "cv1 T1" является ссылочным-совместимым с "cv3 T3" (см. 13.3.1.6), тогда ссылка привязана к значению выражения инициализатора в первом случае и к результат преобразования во втором случае.

Этими кандидатами в [over.match.ref] являются:

Для прямой инициализации эти явные функции преобразования, которые не скрыты внутри S и выдают тип "lvalue reference to cv2 T2" или "cv2 T2" или "rvalue reference to cv2 T2", соответственно, где T2 является тем же типом, что и T, или может быть преобразовано в тип T с квалификацией преобразования (4.5), также являются функциями кандидата.

Что включает наш operator const Y(). Следовательно, конструктор копирования является жизнеспособным. Конструктор перемещения не является (поскольку вы не можете привязать ссылку const rvalue к const rvalue), поэтому у нас есть ровно один жизнеспособный кандидат, который делает программу хорошо сформированной.


Er, в качестве продолжения, это ошибка LLVM 16682, что делает его намного сложнее, чем то, что я изначально изложил.

Ответ 3

Когда вы пишете код, где два достойных компилятора не согласны с тем, является ли он законным или нет, вы программируете слишком близко к краю. Скажем, я программист по обслуживанию, поддерживающий этот код. Как вы ожидаете, что я узнаю, является ли это законным, и какова именно семантика этого кода, если даже gcc и clang не могут согласиться?

Измените свой код. Сделайте это проще, чтобы как менее "умные" программисты, так и компиляторы понимали это без проблем. Нет никаких призов для того, чтобы быть самым "умным" программистом.

Посмотрите на ответ Колумба: я не сомневаюсь, что его анализ ситуации в порядке и правильном. Но я бы не хотел поддерживать код, который требует очень умного анализа на 50 строк, чтобы продемонстрировать, что это правильно. Если вы пишете компиляторы С++, вы должны внимательно изучить его ответ. Если вы пишете код приложения, вы никогда не должны писать код, который требует взглянуть на его ответ.