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.
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 . – Bradfordbradleevalue_t<foobar>
should be equivalent tofoobar::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