Подтвердить что ты не робот

Clang vs GCC против оператора преобразования шаблонов MSVC - какой компилятор прав?

У меня есть простой код с оператором преобразования, и кажется, что все компиляторы дают разные результаты, было любопытно, какой компилятор, если он есть, правильный? Я также пробовал разные комбинации, но ниже самые интересные. Код был скомпилирован с использованием флага С++ 11, но такое же поведение можно наблюдать и в С++ 03.

#include <iostream>

struct call_operator {
    template<typename T>
    operator T() {
        std::cout << __FUNCTION__ << std::endl;
        return {};
    }

    template<typename T>
    operator const T&() const {
        std::cout << __FUNCTION__ << std::endl;
        static T t;
        return t;
    }

    template<typename T>
    operator T&() const {
        std::cout << __FUNCTION__ << std::endl;
        static T t;
        return t;
    }
};

int main() {
    (void)static_cast<int>(call_operator());
    (void)static_cast<const int&>(call_operator());
    (void)static_cast<int&>(call_operator());
}

лязг-3.6:

operator int
operator const int &
operator int &

г ++ - 4,9:

operator T
operator const T&
operator T&

msvc 2014 CTP:

call_operator.cpp(17): error C2440: 'static_cast': cannot convert from 'call_operator' to ' const int &'

после удаления:

template<typename T>
operator T();

msvc компилирует:

call_operator::operator const int &
call_operator::operator const int &
call_operator::operator int &

кроме того, после удаления const в

template<typename T>
operator const T&();

лязг-3.6:

call_operator.cpp:26:9: error: ambiguous conversion for static_cast from 'call_operator' to 'int' (void)static_cast<int>(call_operator());

г ++ - 4,9:

operator T
operator const T&
operator T&

msvc 2014 CTP:

call_operator.cpp(16): error C2440: 'static_cast': cannot convert from 'call_operator' to 'int'
4b9b3361

Ответ 1

Короче: Clang правильный (хотя в одном случае, по неправильной причине). GCC ошибочен во втором случае. В первом случае MSVC ошибочен.

Пусть начнется с static_cast (§5.2.9 [expr.static.cast]/p4, все цитаты из N3936):

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

Соответственно, три static_cast здесь фактически три инициализации:

int t1(call_operator{});
const int & t2(call_operator{});
int & t3(call_operator{});

Обратите внимание, что мы переписывали call_operator() как call_operator{} только для целей изложения, так как int t1(call_operator()); - самый неприятный синтаксический анализ. Существует небольшая смысловая разница между этими двумя формами инициализации, но эта разница несущественна для этого обсуждения.

int t1(call_operator{});

Применимое правило для этой инициализации изложено в §8.5 [dcl.init]/p17:

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

Перейдем к §13.3.1.5 [over.match.conv], в котором говорится:

Предполагая, что "cv1 T" является типом инициализированного объекта, и "cv S" - тип выражения инициализатора, с S a тип класса, выбранные функции выбираются следующим образом:

  • Рассматриваются функции преобразования S и его базовые классы. Те неявные функции преобразования, которые не скрыты внутри Sи тип вывода T или тип, который можно преобразовать в тип T через стандартная последовательность преобразования (13.3.3.1.1) являются функциями кандидата. Для прямая инициализация, те явные функции преобразования, которые не скрытый в S и тип yield T, или тип, который может быть преобразованные в тип T с квалификационным преобразованием (4.4), также кандидатские функции. Функции преобразования, которые возвращают cv-квалификацию тип, как представляется, дают cv-неквалифицированную версию этого типа для этого процесса выбора кандидатских функций. преобразование функции, возвращающие "ссылка на cv2 X" return lvalues ​​или xvalues, в зависимости от типа ссылки, типа "cv2 X" и являются поэтому считается, что X для этого процесса выбора кандидатские функции.

2 Список аргументов имеет один аргумент, который является инициализатором выражение. [Примечание. Этот аргумент будет сравниваться с неявный параметр объекта для функций преобразования. -end note]

Установленный кандидат после вывода аргумента шаблона:

operator T() - with T = int
operator const T& () const - with T = int
operator T&() const - with T = int

Список аргументов состоит из одного выражения call_operator{}, которое не является константой. Поэтому он лучше преобразуется в не-const неявный объектный параметр operator T(), чем к двум другим. Соответственно, operator T() является наилучшим совпадением и выбирается с помощью разрешения перегрузки.

const int & t2(call_operator{});

Эта инициализация определяется §8.5.3 [dcl.init.ref]/p5:

Ссылка на тип "cv1 T1" инициализируется выражением типа "cv2 T2" следующим образом:

  • Если ссылка является ссылкой lvalue и выражением инициализатора

    • является lvalue (но не является битовым полем), а "cv1 T1" ссылается на "cv2 T2" или
    • имеет тип класса (т.е. T2 - тип класса), где T1 не ссылается на T2 и может быть преобразован в значение l типа "cv3 T3", где "cv1 T1" ссылается на "cv3 T3", (это преобразование выбирается путем перечисления применимой конверсии функций (13.3.1.6) и выбора наилучшего из-за перегрузки (13.3)).

то ссылка привязана к выражению инициализатора lvalue в первый случай и результат lvalue преобразования в второй случай (или, в любом случае, к соответствующему базовому классу подобъектом объекта).

Обратите внимание, что на этом шаге рассматриваются только функции преобразования, возвращающие ссылки lvalue.

Кажется, что Clang выводит набор кандидатов как *:

operator const T& () const - with T = int
operator T&() const - with T = int

Очевидно, что обе функции связаны с неявным параметром объекта, поскольку оба параметра const. Кроме того, поскольку оба являются прямыми привязками привязки, в §13.3.3.1.4 [ics.ref]/p1 преобразование, требуемое от типа возврата любой из функций к const int &, является преобразованием идентичности. ( Не Квалификационная настройка - это относится к преобразованию, описанному в §4.4 [conv.qual], и применимо только к указателям.)

Однако, похоже, что вывод, выполненный Clang для operator T&() в этом случае, неверен & Dagger;. §14.8.2.3 [temp.deduct.conv]/p5-6:

5 В целом процесс дедукции пытается найти шаблонный аргумент значения, которые сделают вывод A идентичным A. Однако там это два случая, которые позволяют разницу:

  • Если исходный A является ссылочным типом, A может быть больше CV, чем выведенный A (то есть тип, на который ссылается ссылка)
  • Выведенный A может быть другим указателем или указателем на тип члена, который может быть преобразован в A через квалификационное преобразование.

6 Эти альтернативы рассматриваются только в том случае, если иначе не получится. Если они дают более одного возможного выведенного A, вывод типа не выполняется.

Поскольку вывод типа может преуспеть, выведя T как const int для operator T&() для точного соответствия между выведенным типом и типом назначения, альтернативы не следует рассматривать, T следовало бы вывести как const int, а набор кандидатов фактически

operator const T& () const - with T = int
operator T&() const - with T = const int

И снова обе стандартные последовательности преобразования из результата - преобразования идентичности. GCC (и EDG, благодаря @Jonathan Wakely для тестирования) правильно выводит T в operator T&() как const int в этом случае *.

Независимо от правильности вычета, тем не менее, тай-брейк здесь один и тот же. Поскольку в соответствии с правилами частичного упорядочения для шаблонов функций operator const T& () более специализирован, чем operator T&() (из-за специального правила в §14.8.2.4 [temp.deduct.partial]/p9), первый выигрывает тай-брейк в §13.3.3 [over.match.best]/p1, 2-й список, последний маркер:

F1 и F2 являются специализированными шаблонами функций, а функция шаблон для F1 более специализирован, чем шаблон для F2в соответствии с правилами частичного упорядочения, описанными в 14.5.6.2.

Таким образом, в этом случае Clang получает правильный результат, но для (частично) неправильной причины. GCC получает правильный результат по правильной причине.

int & t3(call_operator{});

Здесь нет борьбы. operator const T&(); просто невозможно использовать для инициализации int &. Существует только одна жизнеспособная функция operator T&() с T = int, поэтому она является наилучшей жизнеспособной функцией.

Что делать, если operator const T&(); не const?

Единственный интересный случай здесь - инициализация int t1(call_operator{});. Двумя сильными соперниками являются:

operator T() - with T = int
operator const T& () - with T = int

Обратите внимание, что правило, касающееся ранжирования стандартных последовательностей преобразования - §13.3.3 [over.match.best]/p1, 2-й список, вторая маркерная точка:

контекст является инициализацией по пользовательскому преобразованию (см. 8.5, 13.3.1.5 и 13.3.1.6) и стандартную последовательность преобразования из возвращаемого типа F1 в тип назначения (то есть тип инициализация объекта) является лучшей последовательностью преобразования, чем стандартная последовательность преобразования из возвращаемого типа F2 в тип назначения.

и §13.3.3.2 [over.ics.rank]/p2:

Стандартная последовательность преобразования S1 является лучшей последовательностью преобразования, чем стандартная последовательность преобразования S2, если

  • S1 является собственной подпоследовательностью S2 (сравнивая последовательности преобразования в канонической форме, определенные в 13.3.3.1.1, за исключением любых Преобразование Lvalue; рассматривается последовательность преобразования идентичности быть подпоследовательностью любой последовательности, не связанной с идентичностью)

не может отличить эти два, потому что преобразование, необходимое для получения int из const int &, является преобразованием lvalue-to-rval, которое является преобразованием Lvalue. После исключения преобразования Lvalue стандартные последовательности преобразования из результата в тип назначения идентичны; ни одно из других правил в п. 13.3.3.2 [over.ics.rank] не применяется.

Таким образом, единственным правилом, которое могло бы различать эти две функции, является снова "более специализированное" правило. Возникает вопрос, является ли один из operator T() и operator const T&() более специализированным, чем другой. Ответ - нет. Подробные правила частичного упорядочения довольно сложны, но аналогичную ситуацию легко найти в примере в §14.5.6.2 [temp.func.order]/p2, который называет вызов g(x) неопределенным:

template<class T> void g(T);
template<class T> void g(T&);

Быстрое прочтение процедуры, указанной в §14.8.2.4 [temp.deduct.partial], подтверждает, что если один шаблон принимает значение const T&, а другой принимает значение T по значению, то он не является более специализированным, чем другой **. Таким образом, в этом случае нет единственной лучшей жизнеспособной функции, преобразование неоднозначно, а код плохо сформирован. & dagger;


* Тип, выводимый Clang и GCC для случая operator T&(), определяется путем удаления кода с operator const T&().Суб >

** Вкратце, во время вычитания для частичного упорядочения, прежде чем какое-либо сравнение будет выполнено, ссылочные типы заменяются типами, на которые ссылаются, а затем верхние уровни cv-квалификаторов лишаются, поэтому обе const T& и T дают одну и ту же подпись. Тем не менее, §14.8.2.4 [temp.deduct.partial]/p9 содержит специальное правило, когда оба типа в вопросе являются ссылочными типами, что делает operator const T&() более специализированным, чем operator T&(); это правило не применяется, если один из типов не является ссылочным типом.

& dagger; GCC, по-видимому, не считает operator const T&() жизнеспособным преобразованием для этого случая, но считает operator T&() жизнеспособное преобразование.

& Dagger; Это, по-видимому, Clang ошибка 20783.Суб >