Why does alias template give a conflicting declaration?
Asked Answered
G

1

41

The port of some C++11 code from Clang to 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, "");
}

gives different behavior for Clang (versions 3.1 through SVN trunk) versus for any g++ version. For the latter I get errors like this

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;

If instead of the template alias value_t<S<T>> I use the full typename S<T>::value_type then g++ also works.

Question: aren't template aliases supposed to be completely interchangeable with their underlying expression? Is this a g++ bug?

Update: Visual C++ also accepts the alias template in the out-of-class definition.

Gerdi answered 13/1, 2017 at 19:45 Comment(9)
Sure seems like they should be equivalent: eel.is/c++draft/temp.alias#2Fawcette
I'll go with compiler bug for 500, AlexJacoby
I don't think it's that trivial. There are a lot of questions about equivalence of dependent types with regard to alias templates. See open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1979 and all the issues that are linked by it. An answer should cover this question and the relevance of those issues on this, IMO.Bradfordbradlee
It doesn't stop there though. There's an age-old issue on whether even the typename S<T>::value_type case is valid or not, and what else you can write instead: open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2 .Bradfordbradlee
I think that in the compilers, alias templates are replaced before the "equivalent" check of 14.5.6.1p5 is done. So value_t<foobar> should be equivalent to foobar::value_type, even if dependent types are involved. This is also what the paragraph quoted by Barry says (it uses "equivalent", although has no footnote to 14.5.6.1p5, which could clarify that it means to refer to that meaning of "equivalent").Bradfordbradlee
@JohannesSchaub-litb Oh, I don't think so either. That's why I left the comment equivalent of a shrug instead of an answer :)Fawcette
should struct s have a template parameter?Melvamelvena
@Melvamelvena it has but it doesn't have to be namedGerdi
I think g++ is just pickier: #642729Ramin
S
5

The problem relies on SFINAE. If you rewrite your member function to be value_t<S<T>>, like the outside declaration, then GCC will happily compile it:

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;

Because the expression is now functionally equivalent. Things like substitution failure come into play on alias-templates, but as you see, the member function value_type const C doesn't have the same "prototype" as value_t<S<T>> const S<T>::C. First one doesn't have to perform SFINAE, whereas the second one requires it. So clearly both declarations have different functionality, hence GCC's tantrum.

Interestingly, Clang compiles it without a sign of abnormality. I assume it just so happens that the order of Clang's analyses are reversed, compared to GCC's. Once the alias-template expression is resolved and fine (i.e. it is well-formed), clang then compares both declarations and check it they are equivalent (which in this case they are, given both expressions resolve to value_type).

Now, which one is correct from the standard's eyes? It's still an unresolved issue to whether consider alias-template's SFNIAE as part of its declaration's functionality. Quoting [temp.alias]/2:

When a template-id refers to the specialization of an alias template, it is equivalent to the associated type obtained by substitution of its template-arguments for the template-parameters in the type-id of the alias template.

In other words, these two are equivalent:

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

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

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

Vec<int> and vector<int, Alloc<int>> are equivalent types, because after substitution is performed, both types end up being vector<int, Alloc<int>>. Note how "after substitution" means that the equivalence is only checked once all template arguments are replaced with the template parameters. That is, comparison starts when T in vector<T, Alloc<T>> is replaced with int from Vec<int>. Maybe that's what Clang is doing with value_t<S<T>>? But then there's the following quote from [temp.alias]/3:

However, if the template-id is dependent, subsequent template argument substitution still applies to the template-id. [Example:

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

 — end example]

Here's the problem: the expression has to be well-formed, so the compiler needs to check whether the substitution is fine. When there is a dependence in order to perform template argument substitution (e.g. typename T::foo), the functionality of the whole expression changes, and the definition of "equivalence" differs. For example, the following code won't compile (GCC and Clang):

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

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

Because the outer foo's prototype is functionally different from the inner one. Doing auto X::foo(T) -> std::enable_if_t<sizeof(T) == 4> instead makes the code compile fine. It's so because the return type of foo is an expression that is dependent on the result of sizeof(T) == 4, so after template substitution, its prototype might be different from each instance of it. Whereas, auto X::foo(T) -> void's return type is never different, which conflicts with the declaration inside X. This is the very same issue that's happening with your code. So GCC seems to be correct in this case.

Salsala answered 1/3, 2017 at 21:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.