Классы с операторами преобразования шаблонов и без шаблонов в состоянии оператора switch

Проблема первоначально возникла в этом вопросе. Рассмотрим следующий код:

class Var
{
public:

    operator int () const
    { return 0; }

    template <typename T>
    operator T () const
    { return T(); }

};

int main()
{
    Var v;
    switch (v)
    { }
}

Без operator int() const { return 0; } оба g++ и clang отклонят код.

Однако приведенный выше код с operator int() является принят клавишей, но отклонено g++ со следующей ошибкой:

main.cpp:17:14: error: default type conversion can't deduce template argument for 'template<class T> Var::operator T() const'
     switch (v)
              ^

Какой компилятор прав?

Ответ 1

Я верю clang здесь.

Из проекта С++ standard можно ознакомиться в разделе 6.4.2 Оператор switch, который включает контекстно-неявное преобразование. В параграфе 2 говорится (* акцент в моем будущем):

Условие должно быть целочисленного типа, типа перечисления или класса тип. Если тип класса, условие контекстуально неявно преобразованный (раздел 4) в интегральный или перечисляемый тип.

Мы можем видеть, что раздел, который нам нужно использовать, - это 4 Стандартные преобразования и параграф 5 охватывают эти случаи, он говорит:

Некоторые языковые конструкции требуют преобразования в значение, имеющее один определенного набора типов, соответствующих конструкции. выражение e типа класса E, появляющееся в таком контексте, называется контекстно-неявно преобразованный в заданный тип T и хорошо сформированный тогда и только тогда, когда e может быть неявно преобразован в тип T который определяется следующим образом: E выполняется поиск функций преобразования чей тип возврата является cv T или ссылкой на cv T, так что T разрешено в контексте. Там должно быть ровно одно такое T.

Это не ссылается на раздел 8.5, который позволяет разрешить перегруз, конкретно ссылаясь на раздел 13.3, не допуская разрешения перегрузки, которое мы не можем использовать:

template <typename T>
operator T () const

и, следовательно, нет двусмысленности.

Обратите внимание, что это отличается от пункта 4, который охватывает преобразования bool в контекстах if, while и т.д. и говорит (внимание мое):

Некоторые языковые конструкции требуют, чтобы выражение было преобразовано в булево значение. Выражение e, появляющееся в таком контексте, для контекстного преобразования в bool и корректно формируется тогда и только тогда, когда декларация bool t (e); хорошо сформирован, для некоторых изобретенных временных переменная t (8.5).

который специально разрешает разрешение перегрузки и ссылается непосредственно на раздел 13.3, который охватывает это. Имеет смысл, что это разрешено, так как у нас есть конкретный тип назначения bool для преобразования, которого мы не имеем в случае коммутатора.

Почему

Мы можем понять это, посмотрев N3323: Предложение по настройке некоторых контекстных конверсий С++, v3, охватывает эту проблему. Было бы трудно процитировать весь документ, поэтому я попытаюсь процитировать достаточно контекста. В нем говорится:

Контекст, в котором выражение С++ появляется часто, влияет на то, как выражение оценивается и, следовательно, может налагать требования к чтобы обеспечить такую ​​оценку. [...]

В четырех случаях FDIS (N3290) использует разные языки для указания аналогичное контекстно-зависимое преобразование. В этих четырех контекстах, когда операнд имеет тип класса, этот тип должен иметь "один неявный преобразование" в подходящий (контекстно-зависимый) тип. [...]

и включает в себя:

[stmt.switch]/2: "Условие должно быть целочисленного типа, перечисление тип или тип класса, для которого одно неявное преобразование существует функция с интегральным или перечисляющим типом (12.3)."

и говорит:

Основная проблема в каждом из четырех контекстов, приведенных в Введение, кажется, лежит в их общей полезной, но очень строгой требование, которое ограничивает класс только одним оператором преобразования [...]

Еще одна проблема заключается в том, что сфера определения "одиночная" в текущем формулировка. Должна быть только одна функция преобразования в классе, или может быть несколько до тех пор, пока один из них контекст?

Текущий язык кажется неясным в этом вопросе. Это также неясно, является ли оператор преобразования, который создает ссылку на соответствующий тип является подходящим оператором преобразования. (Вопрос о этот момент был отправлен на отражатель Core 2011-02-21, но остался без ответа на момент написания этой статьи.) Текущая практика компилятора кажется допускать такие операторы, но текущий язык, похоже, не соответствует.

и предлагает:

Чтобы решить все эти проблемы, мы рекомендуем вместо этого использовать проверенные подход, обозначаемый термином, контекстно преобразованным в bool, как определенной в [conv]/3. Поэтому мы предлагаем скромное дополнение к [conv]/3 для определения контекстного преобразования в другие указанные типы и затем обратитесь к этому новому определению.

и новый язык будет выглядеть следующим образом:

Некоторые конструкции других языков требуют аналогичного преобразования, но для значение, имеющее один из определенного набора типов, соответствующих построить. Выражение e типа класса E, появляющееся в таком контексте называется контекстно неявно преобразованным в заданный тип T и корректно формируется тогда и только тогда, когда e может быть неявно преобразован в тип T, который определяется следующим образом: E выполняется поиск преобразования функции, тип возврата которых является cv T или ссылкой на cv T, что T разрешается контекстом. Там должно быть ровно одно такое T.

Примечание N3486: Отчет редактора С++, октябрь 2012 г. показывает нам, когда N3323 был включен в проект стандарта.

Обновить

Подано сообщение gcc bug.

Ответ 2

6.4.2/2 Оператор switch (выделено мной)

Условие должно быть целым типом, типом перечисления или типом класса, для которого существует одна неявная функция преобразования для целочисленного или перечисляемого типа (12.3). Если условие имеет тип класса, условие преобразуется путем вызова этой функции преобразования, а результат преобразования используется вместо исходного условия для остальной части этого раздела.

Итак, моя интерпретация заключается в том, что g++ здесь верен.

Ответ 3

Я считаю, что gcc верен, но стандарт ошибочен.

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

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

operator T может иметь к нему предложение SFINAE произвольной сложности. Компилятор под стандартом должен определить, существует ли T, чтобы T был enum.

template<class...Ts>
struct evil {
  enum { bob = 3+sizeof...(Ts) };
};

struct test {
  operator int() const { return -1; };
  template<class T, typename std::enable_if<T::bob==2>::type* unused=nullptr>
  operator T() const { return T::bob; }
};
int main() {
  switch( test{} ) {
    case -1: std::cout << "int\n"; break;
    case 2: std::cout << "bob\n"; break;
    default: std::cout << "unexpected\n"; break;
  }
}

Вышеприведенный код демонстрирует случай, когда мы имеем неограниченное количество enum неявно доступных. У нас есть operator T, который будет отличать тип T тогда и только тогда, когда T::bob==2. Теперь нет такой enum в нашей программе (и даже если бы мы удалили 3+, этого бы не было, потому что она не является enum class - легко исправлена).

Итак, test может быть преобразован только в int, и поэтому инструкция switch должна компилироваться. gcc отказывает в этом тесте и утверждает, что template operator T делает его неоднозначным (не сообщая нам, что T, естественно).

Замена enum type на enum class type, и удаление 3+ делает оператор switch незаконным в соответствии со стандартом. Но для компилятора, чтобы понять это, в основном необходимо создать все возможные шаблоны в программе, которые ищут секретный enum с соответствующим свойством. С небольшим количеством работы я могу заставить компилятор решить NP полные проблемы (или, исключая ограничения компилятора, проблему остановки), чтобы определить, должна ли компиляция прогрмы или нет.

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

Ответ 4

В моем суматошном мнении и на основе §13.3.3/1 Наилучшая жизнеспособная функция [over.match.best], оператор перегруженного преобразования без шаблона (т.е. operator int() const) имеет более высокий приоритет в терминах разрешения перегрузки выбор, чем его шаблонный аналог (т.е. template <typename T> operator T () const).

Таким образом, перегруженное разрешение правильно выбрало бы operator int() const над template <typename T> operator T () const, так как это лучшая жизнеспособная функция.

Кроме того, поскольку версия без шаблона будет выбрана над шаблоном (т.е. шаблон не будет материализован/квалифицирован компилятором), class Var будет иметь одну функцию преобразования и, следовательно, требование в § 6.4.2/2 Оператор switch [stmt.switch] для одного интегрального преобразования будет выполнен.

Следовательно, Clang прав, а GCC ошибочен.

Ответ 5

Вот соответствующие цитаты, но окончательный ответ во многом зависит от интерпретации. Я даже не могу выбрать фаворита прямо сейчас.

N3797 6.4.2/2:

Условие должно быть целочисленного типа, типа перечисления или типа класса. Если тип класса, условие контекстно неявно преобразуется (раздел 4) в интегральный или перечисляемый тип.

4/5:

Для некоторых языковых конструкций требуется преобразование в значение, имеющее один из заданного набора типов, подходящих для конструкции. Выражение e типа класса e, появляющееся в таком контексте, называется контекстно неявно преобразованным в указанный тип T и хорошо сформировано тогда и только тогда, когда e может быть неявно преобразовано в тип T, который определяется следующим образом: e выполняется поиск функций преобразования, тип возврата которых является cv T или ссылкой на cv T, так что T разрешен контекстом. Там должно быть ровно одно такое T.

14.5.2/6:

Специализация функции преобразования не найдена при поиске по имени. Вместо этого рассматриваются все шаблоны функций преобразования, видимые в контексте использования. Для каждого такого оператора, если вывод аргумента преуспевает (14.8.2.3), полученная специализация используется как бы найденная при поиске по имени.

14.5.2/8:

Разрешение перегрузки (13.3.3.2) и частичное упорядочение (14.5.6.2) используются для выбора наилучшей функции преобразования между несколькими специализациями шаблонов функций преобразования и/или функциями преобразования без шаблона.

Интерпретация 1: 4/5 гласит: "функции преобразования", а не "функции преобразования и шаблоны функций преобразования". Поэтому Var::operator int() const - единственный вариант, а clang - правильный.

Интерпретация 2 [слабый?]: 14.5.2 требует, чтобы мы сравнивали шаблон функции преобразования с помощью разрешения перегрузки и частичного упорядочения в том же начальном положении, что и функция преобразования без шаблона. Они сравнивают специализированные функции и функции шаблонов, а не шаблоны функций, поэтому мы будем делать вывод аргумента шаблона. Вычисление аргумента шаблона для шаблона функции преобразования требует целевого типа. Хотя у нас обычно есть более четкий тип цели, в этом случае мы просто попробуем (теоретически все) все типы в наборе допустимых типов. Но ясно, что функция без шаблона является лучшей жизнеспособной функцией, которая включает все специализированные шаблоны, поэтому разрешение перегрузки выбирает функцию без шаблона. clang является правильным.

Интерпретация 3: Поскольку разрешение перегрузки требует вычитания аргумента шаблона, а для вычитания аргумента шаблона требуется известный тип цели, сначала следует рассмотреть семантику 4/5, а затем ее преобразованный тип (если есть) можно использовать для перегрузки процесс разрешения. 14.5.2 требует рассмотрения шаблона функции преобразования, но затем мы обнаруживаем, что существует несколько допустимых типов T, для которых у нас есть функция преобразования в T [эта функция может быть специализированной функцией]. Программа плохо сформирована, поэтому g++ корректна.

Ответ 6

Если я правильно прочитал этот раздел при перегрузке, Clang корректен

13.3.3 Лучшая жизнеспособная функция [over.match.best]

[...] Учитывая эти определения, жизнеспособная функция F1 определяется как лучшая функция, чем другая жизнеспособная функция F2, если для всех аргументов я ICSi (F1) не является худшей последовательностью преобразования, чем ICSi (F2), и затем [...]

- F1 - это не шаблонная функция, а F2 - специализированная функция шаблона или, если не это, [...]

Проект свободен для чтения. Не уверен, что какие-либо изменения в 13.3.3 были помещены в окончательную спецификацию (я не заплатил за нее)

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

Я бы скопировал ошибку g++:-) Они могут вернуться с другим разделом стандарта для оправдания, но, похоже, не соответствуют стандартам.

Редактировать комментарий aschepler:

От: http://publib.boulder.ibm.com/infocenter/comphelp/v101v121/index.jsp?topic=/com.ibm.xlcpp101.aix.doc/language_ref/cplr315.html

Предположим, что f является перегруженным именем функции. Когда вы вызываете перегруженную функцию f(), компилятор создает набор функций-кандидатов. Этот набор функций включает в себя все функции с именем f, к которым можно получить доступ из точки, где вы вызвали f(). Компилятор может включать в качестве функции-кандидата альтернативное представление одной из этих доступных функций с именем f для облегчения разрешения перегрузки.

После создания набора функций-кандидатов компилятор создает набор жизнеспособных функций. Этот набор функций является подмножеством функций-кандидатов. Количество параметров каждой жизнеспособной функции согласуется с количеством аргументов, которые вы использовали для вызова f().