Наследование шаблонного оператора = в С++ 14: различное поведение с g++ и clang++

У меня есть этот код, который работает, как и ожидалось, с GCC 9.1:

#include <type_traits>

template< typename T >
class A
{
protected:
    T value;

public:
    template< typename U,
              typename...,
              typename = std::enable_if_t< std::is_fundamental< U >::value > >
    A& operator=(U v)
    {
        value = v;
        return *this;
    }
};

template< typename T >
class B : public A<T>
{
public:
    using A<T>::operator=;

    template< typename U,
              typename...,
              typename = std::enable_if_t< ! std::is_fundamental< U >::value > >
    B& operator=(U v)
    {
        this->value = v;
        return *this;
    }
};

int main()
{
    B<int> obj;
    obj = 2;
}

(На практике мы сделали бы что-нибудь необычное в B::operator= и даже использовали бы другие черты типа для enable_if, но это самый простой воспроизводимый пример.)

Проблема в том, что Clang 8.0.1 выдает ошибку, почему-то не учитывается operator= от родительского класса, хотя у дочернего есть using A<T>::operator=;:

test.cpp:39:9: error: no viable overloaded '='
    obj = 2;
    ~~~ ^ ~
test.cpp:4:7: note: candidate function (the implicit copy assignment operator) not viable:
      no known conversion from 'int' to 'const A<int>' for 1st argument
class A
      ^
test.cpp:4:7: note: candidate function (the implicit move assignment operator) not viable:
      no known conversion from 'int' to 'A<int>' for 1st argument
class A
      ^
test.cpp:20:7: note: candidate function (the implicit copy assignment operator) not
      viable: no known conversion from 'int' to 'const B<int>' for 1st argument
class B : public A<T>
      ^
test.cpp:20:7: note: candidate function (the implicit move assignment operator) not
      viable: no known conversion from 'int' to 'B<int>' for 1st argument
class B : public A<T>
      ^
test.cpp:28:8: note: candidate template ignored: requirement
      '!std::is_fundamental<int>::value' was not satisfied [with U = int, $1 = <>]
    B& operator=(U v)
       ^
1 error generated.

Какой компилятор соответствует стандарту? (Я компилирую с -std=c++14.) Как мне изменить код, чтобы он стал правильным?


person Jakub Klinkovský    schedule 02.08.2019    source источник
comment
Обратите внимание, что ваш код отклоняется не только clang: godbolt.org/z/bLp_i3   -  person Bob__    schedule 02.08.2019
comment
Интересно, а правы они или нет?   -  person Jakub Klinkovský    schedule 02.08.2019
comment
Думаю, они правы. Вы можете написать это wandbox.org/permlink/PPXl1iWpxO0icf3S   -  person Bob__    schedule 02.08.2019
comment
Для вашего предыдущего комментария обратите внимание, что проблему с icc можно исправить, добавив еще один параметр шаблона, чтобы сделать подписи разными: godbolt .org/z/REouFh Однако он по-прежнему не работает с clang или msvc.   -  person Jakub Klinkovský    schedule 02.08.2019
comment
Я думаю, что это тоже связано: stackoverflow.com/questions/15427667/   -  person Bob__    schedule 02.08.2019
comment
Связано: stackoverflow.com/q/34158902. Здесь проблема более тонкая, потому что operator= не может иметь аргумента по умолчанию.   -  person L. F.    schedule 02.08.2019
comment
Кроме того, что здесь должен делать пакет параметров случайного шаблона typename...? Они всегда выводятся как пустые, так почему?   -  person L. F.    schedule 02.08.2019
comment
typename... запрещает пользователям обходить SFINAE с помощью чего-то вроде obj.template operator=<int, foo>(2). Я признаю, что это более полезно для обычных функций, чем для операторов, и здесь не имеет значения.   -  person Jakub Klinkovský    schedule 02.08.2019
comment
@JakubKlinkovský Вау, это так умно! Если это так, вы можете использовать std::enable_if_t<..., int> = 0 вместо этого.   -  person L. F.    schedule 02.08.2019
comment
использование std::enable_if_t<cond<T>::value, int> = 0> (по сравнению с typename = std::enable_if_t<cond<T>::value>) также позволит избежать захвата (и, кроме того, позволит перегрузить)   -  person Jarod42    schedule 02.08.2019
comment
Связанные с являются -type-aliases-used-as-type-of-function-parameter-part-of-the-function-signature   -  person Jarod42    schedule 02.08.2019
comment
@ Jarod42 Jarod42 Что вы имеете в виду, говоря о том, что разрешаете перегрузки?   -  person Jakub Klinkovský    schedule 02.08.2019
comment
Я настоятельно рекомендую вам выработать привычку использовать SINAE, например template <typename T, std::enable_if_t<my_condition, bool> = true> return_type function_name(params). Это позволяет вам писать наборы перегрузки, такие как template <typename T, std::enable_if_t<std::is_intergral_v<T>, bool> = true> return_type function_name(params) template <typename T, std::enable_if_t<std::is_floating_point_v<T>, bool> = true> return_type function_name(params), поскольку значения шаблона по умолчанию являются частью подписи шаблона.   -  person NathanOliver    schedule 02.08.2019
comment
@NathanOliver Можете ли вы избежать написания части std::enable_if_t в двух разных местах, если вы хотите написать определение функции-члена вне объявления класса?   -  person Jakub Klinkovský    schedule 02.08.2019
comment
@JakubKlinkovský Я в это не верю.   -  person NathanOliver    schedule 02.08.2019
comment
@NathanOliver Так что я не думаю, что это хорошая универсальная привычка. Однако спасибо за совет, я буду рассматривать его для простых функций.   -  person Jakub Klinkovský    schedule 02.08.2019


Ответы (2)


Рассмотрим этот упрощенный код:

#include <iostream>

struct A
{
    template <int n = 1> void foo() { std::cout << n; }
};

struct B : public A
{
    using A::foo;
    template <int n = 2> void foo() { std::cout << n; }
};

int main()
{
    B obj;
    obj.foo();
}

Это печатает 2, как и должно быть с обоими компиляторами.

Если в производном классе уже есть класс с такой же сигнатурой, то он скрывает или переопределяет класс, введенный объявлением using. Подписи ваших операторов присваивания якобы одинаковы. Рассмотрим этот фрагмент:

template <typename U, 
          typename = std::enable_if_t<std::is_fundamental<U>::value>>
void bar(U) {}
template <typename U, 
          typename = std::enable_if_t<!std::is_fundamental<U>::value>>
void bar(U) {}

Это вызывает ошибку переопределения для bar в обоих компиляторах.

ОДНАКО, если изменить тип возвращаемого значения в одном из шаблонов, ошибка исчезнет!

Пришло время внимательно посмотреть на стандарт.

Когда использующий-декларатор переносит объявления из базового класса в производный класс, функции-члены и шаблоны функций-членов в производном классе переопределяют и/или скрывают функции-члены и шаблоны функций-членов с тем же именем, список-типов-параметров (11.3. 5), cv-qualification и ref-qualifier (если есть) в базовом классе (а не конфликтующие). Такие скрытые или переопределенные объявления исключаются из набора объявлений, введенных с помощью объявления-использования.

Что касается шаблонов, то это звучит сомнительно. Как вообще можно сравнивать два списка типов параметров, не сравнивая списки параметров шаблона? Первое зависит от второго. Действительно, абзац выше говорит:

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

Это имеет гораздо больше смысла. Два шаблона одинаковы, если их списки параметров шаблона одинаковы, как и все остальное... но подождите, сюда входит и тип возвращаемого значения! Два шаблона одинаковы, если их имена и все в их подписях, включая возвращаемые типы (но не включая значения параметров по умолчанию), одинаковы. Тогда одно может конфликтовать с другим или скрывать его.

Так что же произойдет, если мы изменим возвращаемый тип оператора присваивания в B и сделаем его таким же, как в A? GCC перестает принимать код.

Итак, мой вывод таков:

  1. Стандарт неясен, когда речь идет о шаблонах, скрывающих другие шаблоны, созданные с помощью объявлений. Если бы это означало исключить параметры шаблона из сравнения, это должно было бы быть указано и разъяснено возможные последствия. Например, может ли функция скрыть шаблон функции или наоборот? В любом случае в стандартном языке существует необъяснимое несоответствие между using в области пространства имен и using, которое приводит имена базовых классов к производному классу.
  2. GCC, кажется, берет правило для using в области пространства имен и применяет его в контексте базового/производного класса.
  3. Другие компиляторы делают что-то другое. Не слишком ясно, что именно; возможно, сравните списки типов параметров без учета параметров шаблона (или возвращаемых типов), как говорится в букве стандарта, но я не уверен, что это имеет смысл.
person n. 1.8e9-where's-my-share m.    schedule 02.08.2019
comment
Когда объявление использования переносит имена из базового класса в область действия производного класса, функции-члены и шаблоны функций-членов в производном классе переопределяют и/или скрывают функции-члены и шаблоны функций-членов с тем же именем, список-типов-параметров ([ dcl.fct]), cv-qualification и ref-qualifier (если есть) в базовом классе (а не конфликтующие). Не могли бы вы рассказать мне, как определяется список типов параметров для шаблонов функций? Происходит ли вывод аргумента шаблона? - person L. F.; 02.08.2019
comment
@ Л.Ф. Это похоже на дефект формулировки, см. абзац выше. Если объявление шаблона функции в области пространства имен имеет то же имя, список типов параметров, тип возвращаемого значения и список параметров шаблона, что и шаблон функции, представленный объявлением использования, программа плохо оформлена. Шаблон может скрывать другой шаблон. Шаблон экземпляр не может скрывать другой экземпляр шаблона. - person n. 1.8e9-where's-my-share m.; 02.08.2019
comment
Я думаю, это прекрасно объясняет, почему GCC принимает код, поэтому я приму ответ. Поскольку стандарт должен быть улучшен, вы можете сообщить о проблеме? - person Jakub Klinkovský; 02.08.2019

Примечание. Я считаю, что этот ответ неверен, а ответ н.м. является правильным . Я сохраню этот ответ, потому что я не уверен, но, пожалуйста, проверьте этот ответ.


Согласно [namespace.udecl]/15:

Когда объявление-использования переносит имена из базового класса в область действия производного класса, функции-члены и шаблоны функций-членов в производном классе переопределяют и/или скрывают функции-члены и шаблоны функций-членов с тем же именем, список типов параметров ([dcl.fct]), cv-qualification и ref-qualifier (если есть) в базовом классе (а не конфликтующие).

operator=, объявленный в производном классе B, имеет точно такое же имя, список типов параметров, квалификацию cv (нет) и квалификатор ссылки (нет), что и объявленный в A. Таким образом, функция, объявленная в B, скрывает код в A, а код имеет неправильный формат, поскольку разрешение перегрузки не находит подходящей функции для вызова. Однако списки параметров шаблона здесь не рассматриваются.

Так стоит ли их учитывать? Здесь стандарт становится неясным. Clang считает, что A и B имеют одинаковую (шаблонную) подпись, но не GCC. nm answer указывает, что реальная проблема на самом деле заключается в типе возвращаемого значения. (Аргументы шаблона по умолчанию никогда не учитываются при определении подписи.)

Обратите внимание, что это решается при поиске имени. Вывод шаблонного аргумента еще не выполняется, равно как и подстановка. Вы не можете сказать: «О, дедукция/подстановка не удалась, так что давайте добавим больше элементов в набор перегрузки». Так что SFINAE здесь не имеет значения.

person L. F.    schedule 02.08.2019
comment
Давайте продолжим обсуждение в чате. - person L. F.; 02.08.2019
comment
Это звучит разумно, но я пока не понимаю, как это работает. Вы говорите, что после вывода аргумента шаблона, а затем вывод аргумента шаблона еще не выполняется... - person Jakub Klinkovský; 02.08.2019
comment
@JakubKlinkovský Извините, я напутал. Что после вычета аргумента шаблона следует удалить. - person L. F.; 02.08.2019
comment
Еще одна придирка заключается в том, что, поскольку поиск имени происходит до вывода аргумента шаблона, список типов параметров должен быть не int, а чем-то вроде typename U. Думаю, именно поэтому @n.m. говорит, что стандарт неясен... - person Jakub Klinkovský; 02.08.2019
comment
ИМО, стандарт ясен (но неправильно игнорировать список аргументов шаблона (кстати, не связанный с аргументом шаблона по умолчанию)) B::operator=(U v) скрывает A::operator=(U v). Мы должны игнорировать различия типов возвращаемого значения (или, по крайней мере, ковариантного типа возвращаемого значения). - person Jarod42; 02.08.2019
comment
@Jarod42 Jarod42 Проблема, я думаю, в том, что список типов параметров касается функций, а шаблон функции сам по себе не является функцией, поэтому нет особого смысла говорить о списке типов параметров шаблона функции, когда инстанцирование не имеет значения. - person L. F.; 02.08.2019
comment
using приносит имена. Они могли бы быть сформулированы по-разному для особого случая шаблона функции. простой и для шаблонов функций-членов также список аргументов-шаблонов выполнил бы эту работу IMO. но сделать это сейчас сложнее, чтобы избежать критических изменений. - person Jarod42; 02.08.2019