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

Неоднозначная перегрузка оператора в Clang

Рассмотрим следующее:

template<typename T>
struct C {};
template<typename T, typename U>
void operator +(C<T>&, U);

struct D: C<D> {};

struct E {};
template<typename T>
void operator +(C<T>&, E);

void F() { D d; E e; d + e; }

Этот код компилируется как на GCC-7, так и на Clang-5. Выбранная перегрузка для operator + - это значение struct E.

Теперь, если происходит следующее изменение:

/* Put `operator +` inside the class. */
template<typename T>
struct C {
    template<typename U>
    void operator +(U);
};

то есть если operator + определяется внутри шаблона класса, а не наружу, то Clang дает двусмысленность между тем, что operator + присутствует в коде. GCC по-прежнему компилируется.

Почему это происходит? Это ошибка в GCC или Clang?

4b9b3361

Ответ 1

Это ошибка в gcc; в частности, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53499.

Проблема заключается в том, что gcc относится к неявному объекту-параметру функции-члена шаблона класса как к зависимому типу; то есть при частичном упорядочивании шаблона функции gcc-преобразования

C<D>::template<class U> void operator+(U);  // #1

в

template<class T, class U> void operator+(C<T>&, U);  // #1a (gcc, wrong)

когда он должен быть преобразован в

template<class U> void operator+(C<D>&, U);  // #1b (clang, correct)

Мы можем видеть, что по сравнению с вашим

template<class T> void operator+(C<T>&, E);  // #2

#2 лучше ошибочного #1a, но неоднозначно с #1b.

Соблюдайте, что gcc неправильно принимает даже тогда, когда C<D> не является шаблоном вообще, т.е. когда C<D> является полной спецификацией шаблона шаблона:

template<class> struct C;
struct D;
template<> struct C<D> {
    // ...

Это описывается [temp.func.order]/3 с разъяснением в примере. Заметим, что gcc неправильно компилирует этот пример, неправильно отклоняя его, но по той же причине.

Ответ 2

Изменить: в исходной версии этого ответа сказано, что GCC был прав. Теперь я верю, что Кланг правилен в соответствии с формулировкой стандарта, но я вижу, как интерпретация GCC также может быть правильной.

Посмотрите на свой первый пример, где два объявления:

template<typename T, typename U>
void operator +(C<T>&, U);
template<typename T>
void operator +(C<T>&, E);

Оба являются жизнеспособными, но очевидно, что второй шаблон более специализирован, чем первый. Таким образом, GCC и Clang разрешают вызов ко второму шаблону. Но позвольте пройти через [temp.func.order], чтобы понять, почему в формулировке стандарта второй шаблон более специализирован.

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

void(C<X1>&, X2)

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

void(C<X3>&, E)

и вычет по первому шаблону завершен (с T= X3 и U= E). Поскольку вывод был выполнен только в одном направлении, шаблон, который принял другой преобразованный тип (первый), считается менее специализированным, и, следовательно, вторая перегрузка выбрана как более специализированная.

Когда вторая перегрузка перемещается в класс C, обе перегрузки все еще находятся, и процесс разрешения перегрузки должен применяться точно так же. Во-первых, список аргументов сконструирован для обеих перегрузок, и поскольку первая перегрузка является нестационарным членом класса, вставляется вставленный косвенный объект. Согласно [over.match.funcs], тип этого предполагаемого параметра объекта должен быть "lvalue reference to C<T>", поскольку функция не имеет ref-qualifier. Таким образом, два списка аргументов являются (C<D>&, E). Так как это не позволяет выбрать один из двух перегрузок, снова выполняется повторный тест частичного заказа.

Тест частичного заказа, описанный в [temp.func.order], также вставляет подразумеваемый параметр объекта:

Если только один из шаблонов функций M является нестационарным членом некоторый класс A, M считается новым первым параметром, вставленным в его список параметров функции. С учетом cv как cv-квалификаторы M (если таковые имеются), новый параметр имеет тип "rvalue reference to cv A", если необязательный ref-qualifier M равен &&, или если M не имеет ref-определителя, а первый параметр другого шаблона имеет значение rvalue ссылочный тип. В противном случае новый параметр имеет тип "lvalue reference to cv A". [Примечание: это позволяет нестатический член, подлежащий упорядочению по отношению к функции, не являющейся членом, и для эквивалентных результатов к заказу двух эквивалентных нечленов. - конечная нота]

Это шаг, где, предположительно, GCC и Clang принимают разные интерпретации стандарта.

My take: член operator+ уже найден в классе C<D>. Параметр шаблона T для класса C не выводится; это известно, потому что процесс поиска имени ввел конкретный базовый класс C<D> из D. Таким образом, фактический operator+, который представляется частично упорядоченному, не имеет свободного параметра T; это не void operator+(C<T>&, U), а скорее void operator+(C<D>&, U).

Таким образом, для перегрузки члена преобразованный тип функции должен быть не void(C<X1>&, X2), а скорее void(C<D>&, X2). Для перегрузки без члена преобразованный тип функции по-прежнему void(C<X3>&, E), как и раньше. Но теперь мы видим, что void(C<D>&, X2) не соответствует шаблону non-member void(C<T>&, E) и не соответствует void(C<X3>&, E) для шаблона-члена void(C<D>&, U). Таким образом, частичное упорядочение не выполняется, а разрешение перегрузки возвращает неоднозначный результат.

Решение GCC продолжать выбирать перегрузку, отличную от члена, имеет смысл, если вы предполагаете, что она строит преобразованный тип функции для элемента лексически, делая его еще void(C<X1>&, X2), в то время как Clang заменяет D на шаблон, оставляя только U как свободный параметр, перед началом частичного теста на упорядочение.