为什么别名模板会造成冲突声明?

41
将某些C++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, "");
}

对于任何g++版本,我会遇到像这样的错误,而对于Clang(3.1到SVN trunk版本),则会出现不同的行为。

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;
如果我使用完整的 typename S<T>::value_type 代替模板别名 value_t<S<T>>,那么g++也可以工作
问题: 模板别名不应该完全可以与其基础表达式互换吗?这是g++的错误吗?
更新: Visual C++在类外定义中也接受模板别名。

4
看起来它们应该是等价的:http://eel.is/c++draft/temp.alias#2 - Barry
4
我选择编译器错误,500分,Alex。 - AndyG
8
我认为这不是那么简单的问题。关于别名模板中依赖类型的等价性存在很多问题。请参见http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1979以及所有与其相关联的问题。回答应该涵盖这个问题以及这些问题对此的相关性,我个人认为这很重要。 - Johannes Schaub - litb
5
但这还不止。关于typename S<T>::value_type的用法是否有效以及您可以替代它写什么,存在一个古老的问题:http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2。 - Johannes Schaub - litb
3
我认为在编译器中,别名模板会在进行14.5.6.1p5的“等效”检查之前被替换。因此,即使涉及依赖类型,“value_t<foobar>”应该等同于“foobar::value_type”。这也是Barry引用的段落所说的(它使用“等效”,虽然没有脚注到14.5.6.1p5,这可能说明它指的是“等效”的那个含义)。 - Johannes Schaub - litb
显示剩余4条评论
1个回答

5
这个问题涉及到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;

因为表达式现在已经 功能上等同。例如,对于别名模板会涉及到 替换失败(substitution failure),但是正如你所看到的,成员函数 value_type const C 的“原型”并不同于 value_t<S<T>> const S<T>::C。第一个不必执行 SFINAE,而第二个则需要执行。因此,两个声明显然具有不同的功能,这就是GCC的问题所在。
有趣的是,Clang编译它时没有任何异常迹象。我想这只是碰巧Clang分析顺序与GCC相反。一旦别名模板表达式被解析并且良好 (即它是well-formed),clang将比较两个声明并检查它们是否等效 (在本例中,给定两个表达式都解析为value_type)。
现在,从标准的角度来看,哪一个是正确的?关于是否将别名模板的SFNIAE视为其声明的功能的问题仍未解决。引用[temp.alias]/2:

当一个模板ID引用了别名模板的特化时,它等同于在类型ID中使用模板参数替换模板参数后获得的相关联类型。

换句话说,这两者是等价的:

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>>。注意,“替换完成后”意味着只有当所有模板参数都被替换为模板参数时才检查等价性。也就是说,在vector<T,Alloc<T>>中的T被替换为Vec<int>中的int之后才开始比较。也许这就是Clang对value_t<S<T>>所做的事情?但是接下来有以下[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]

这里的问题在于:表达式必须是良构的,因此编译器需要检查替换是否正确。当存在依赖关系以执行模板参数替换(例如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似乎是正确的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接