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

Почему шаблон псевдонимов дает противоречивую декларацию?

Порт некоторого кода С++ 11 от Clang до g++

template<class T>
using value_t = typename T::value_type;

template<class>
struct S
{
    using value_type = int;
    static value_type const C = 0;
};

template<class T> 
value_t<S<T>> // gcc error, typename S<T>::value_type does work
const S<T>::C;

int main() 
{    
    static_assert(S<int>::C == 0, "");
}

дает различное поведение для Clang (версии 3.1 через соединительную линию SVN) по сравнению с любой версией g++. Для последнего я получаю ошибки как этот

prog.cc:13:13: error: conflicting declaration 'value_t<S<T> > S< <template-parameter-1-1> >::C'
 const S<T>::C;
             ^
prog.cc:8:29: note: previous declaration as 'const value_type S< <template-parameter-1-1> >::C'
     static value_type const C = 0;
                             ^
prog.cc:13:13: error: declaration of 'const value_type S< <template-parameter-1-1> >::C' outside of class is not definition [-fpermissive] const S<T>::C;

Если вместо псевдонима шаблона value_t<S<T>> я использую полный typename S<T>::value_type, тогда g++ также работает.

Вопрос: не являются ли псевдонимы шаблонов полностью взаимозаменяемыми с их основным выражением? Это ошибка g++?

Обновить. Visual С++ также принимает шаблон псевдонима в определении вне класса.

4b9b3361

Ответ 1

Проблема зависит от SFINAE. Если вы переписываете свою функцию-член как value_t<S<T>>, как и внешнюю декларацию, GCC будет с удовольствием ее компилировать:

template<class T>
struct S
{
    using value_type = int;
    static const value_t<S<T>> C = 0;
};

template<class T> 
const value_t<S<T>> S<T>::C;

Потому что выражение теперь функционально эквивалентно. Такие вещи, как сбой замены, вступают в игру на шаблонах псевдонимов, но, как видите, функция-член value_type const C не имеет того же "prototype" как value_t<S<T>> const S<T>::C. Во-первых, не нужно выполнять SFINAE, тогда как второй требует. Таким образом, обе декларации имеют разную функциональность, поэтому истерика GCC.

Интересно, что Clang компилирует его без признаков ненормальности. Я предполагаю, что так получилось, что порядок анализов Кланг изменился, по сравнению с GCC. После того как выражение alias-template будет разрешено и точным (то есть оно хорошо сформировано), clang затем сравнивает обе декларации и проверяет, что они эквивалентны (что в этом случае они, если оба выражения разрешены на value_type).

Теперь, какой из правильных глаз? Он по-прежнему остается нерешенным вопросом, следует ли рассматривать шаблон alias-SFNIAE как часть его функций декларации. Цитирование [temp.alias]/2:

Когда идентификатор шаблона ссылается на специализацию шаблона псевдонима, он эквивалентен связанному типу, полученному путем подстановки его шаблонных аргументов для параметров шаблона в идентификаторе типа шаблона псевдонима.

Другими словами, эти два эквивалента:

template<class T>
struct Alloc { /* ... */ };

template<class T>
using Vec = vector<T, Alloc<T>>;

Vec<int> v;
vector<int, Alloc<int>> u;

Vec<int> и vector<int, Alloc<int>> являются эквивалентными типами, потому что после подстановки оба типа заканчиваются vector<int, Alloc<int>>. Обратите внимание, что "после подстановки" означает, что эквивалентность проверяется только после того, как все аргументы шаблона заменяются параметрами шаблона. То есть сравнение начинается, когда T в vector<T, Alloc<T>> заменяется на int из Vec<int>. Может быть, что Кланг делает с value_t<S<T>>? Но затем следующая цитата из [temp.alias]/3:

Однако, если идентификатор шаблона зависит, последующая замена аргумента шаблона по-прежнему применяется к идентификатору шаблона. [Пример:

template<typename...> using void_t = void;
template<typename T> void_t<typename T::foo> f();
f<int>(); // error, int does not have a nested type foo

- конец примера]

Здесь проблема: выражение должно быть хорошо сформировано, поэтому компилятору необходимо проверить, хорошо ли подстановка. Когда существует зависимость для выполнения замены аргумента шаблона (например, typename T::foo), функциональность всего выражения изменяется, а определение "эквивалентность" отличается. Например, следующий код не будет компилироваться (GCC и Clang):

struct X
{
    template <typename T>
    auto foo(T) -> std::enable_if_t<sizeof(T) == 4>;
};

template <typename T>
auto X::foo(T) -> void
{}

Потому что внешний прототип foo функционально отличается от внутреннего. Выполнение auto X::foo(T) -> std::enable_if_t<sizeof(T) == 4> вместо этого делает компиляцию кода в порядке. Это так, потому что тип возврата foo является выражением, которое зависит от результата sizeof(T) == 4, поэтому после замены шаблона его прототип может отличаться от каждого его экземпляра. В то время как тип возврата auto X::foo(T) -> void никогда не отличается, что противоречит декларации внутри X. Это та самая проблема, что происходит с вашим кодом. Таким образом, GCC представляется правильным в этом случае.