当专门化静态constexpr模板成员时,会出现未定义的引用错误。

7
以下代码会出现未定义的引用链接错误:
template<int>
struct X {
    static constexpr int x = 0;
};

template<>
constexpr int X<1>::x;

int main() 
{   
    return X<1>::x;
}

但我不知道确切的原因。

是否可以定义一个数据成员而不必专门化整个模板?

明确一点:这段代码可以编译通过,但会产生链接错误(未定义引用)。


1
也许你的“与”问题应该是主要问题。 - DeiDei
是的,已编辑;-) - wimalopaan
3个回答

6
能否在不对整个模板进行[特化]的情况下定义数据成员?
可以显式特化类模板的static数据成员([temp.expl.spec]),但是如果你想这样做,你不能已经在类模板内部为该成员指定了初始化程序(class.static.data)。
也就是说,如果我们将constexpr替换为const,那么这段代码就没问题了:
template<int>
struct X {
    static const int x;
};

template<int I>
const int X<I>::x = 0;

template<>
const int X<1>::x = 1;

但是这段代码并不好:

但是这段代码并不好: NOT

template<int>
struct X {
    static const int x = 0;
};

template<>
const int X<1>::x = 1;

您可以看到,我们初始化主模板变量的区别在于哪里。
现在,如果我们希望将 const 替换为 constexpr ,那么我们必须提供一个初始化程序(class.static.data):

文字类型的静态数据成员可以在类定义中使用 constexpr 说明符声明;如果这样做,则它的声明应指定一个花括号或等号初始化程序,在其中每个分配表达式的初始化器子句都是常量表达式。

所以我们最终陷入了这种奇怪的情况:我们可以专门化 static 成员,但如果它是 constexpr ,则不能,因为 constexpr 需要一个初始化程序。在我看来,这是标准的缺陷。
然而,似乎并不是所有的现代编译器都同意这一点。
gcc 8.0.0编译(但不链接)您的代码(错误),但是如果您为专业化添加初始化程序,则会抱怨重复初始化(正确)。
clang 6.0.0不会按原样编译代码(正确),但是当您添加初始化程序时,它可以无障碍地工作(错误,但这可能是标准应该规定的内容)。
MSVC 19.00.23506不会按原样编译代码(正确),并且当您添加初始化程序时,它也不会编译代码(抱怨重定义)(正确)。
最终,将专业化推入辅助Traits类可能更容易一些:
template<int>
struct X_Traits{
    static constexpr int value = 0;
};

template<>
struct X_Traits<1>{
    static constexpr int value = 1;
};

template<int I>
struct X {
    static constexpr int x=X_Traits<I>::value;
    // ...
};

在C++17及以上版本中,我们可以使用constexpr if来避免需要专门化特性类:

template<int I>
struct X_Traits{
    static constexpr int get_value(){
        if constexpr(I==1){
            return 1;
        }else{
            return 0;
        }
    }
};

template<int I>
struct X {
    static constexpr int x=X_Traits<I>::get_value();
    // ...
};

int main(){
    static_assert(X<0>::x == 0);
    static_assert(X<1>::x == 1);
}

但这并不能解释为什么上面的代码可以编译通过,但会出现链接器错误。 - wimalopaan
1
@wimalopaan:上述代码似乎可以在gcc中编译(一个错误),但在clang或msvc中无法编译。我的意思是,该代码形式不正确,因为它试图重新定义现有的“static constexpr”成员(“无法特化模板类的单个成员”)。 - AndyG
但是StoryTeller从C++标准中给我们的摘录明确提到了特化。因此,单个成员的特化一定是可能的吗?还是我对上面标准的引用理解有误? - wimalopaan
@wimalopaan:我认为你读得很正确。我觉得我的措辞需要改进。你可以为几乎任何事情提供完全显式的特化。 - AndyG
好的,谢谢。我刚刚更深入地查看了http://en.cppreference.com/w/cpp/language/template_specialization,现在我明白了。 - wimalopaan

4
你因显式特化而遇到了一个“小”问题。如果我们参考[temp.expl.spec]/13

An explicit specialization of a static data member of a template or an explicit specialization of a static data member template is a definition if the declaration includes an initializer; otherwise, it is a declaration. [ Note: The definition of a static data member of a template that requires default-initialization must use a braced-init-list:

template<> X Q<int>::x;                         // declaration
template<> X Q<int>::x ();                      // error: declares a function
template<> X Q<int>::x { };                     // definition

 — end note ]

意思是你声明了 X<1>::x 存在,但没有定义它。因此它是未定义的。
我觉得很疯狂的是,你的编译器竟然接受这个声明。一般情况下,你不能声明 constexpr 变量而不定义它们。这非常奇怪。

1
@AndyG - 这令人失望。确实,这是两个相互对立的力量(专业化是一个声明,constexpr需要一个初始化器),但协调起来不应该那么难。 - StoryTeller - Unslander Monica
似乎是语言缺陷? - wimalopaan
@wimalopaan - 我不确定这是语言缺陷。规则并不是真正互相排斥的。我认为这是实现质量问题。这是一个尝试声明但不初始化constexpr变量的方法。因此应该有诊断。如果您对其进行初始化,则是定义,因此程序应该是良好形式的。故事结束。 - StoryTeller - Unslander Monica
1
@StoryTeller:稍微看一下class.static.data就会更加混淆。在C++11中,static constexpr成员仍然必须在命名空间范围内定义,但不需要初始化,而OP却没有这样做。这在C++17中已经被弃用。 - AndyG
2
@AndyG - 嗯,弃用是因为在C++17中static constexpr成员变成了隐式的inline - StoryTeller - Unslander Monica
显示剩余3条评论

2

like this.

template<int i>
struct X {
    static constexpr int x = i==0?2:10;
};

int main() 
{   
    return X<1>::x;
}

请注意,您不需要在类外部定义它。

是的,我知道。但我想知道上述错误的根本原因。 - wimalopaan
很好地利用了常量表达式来初始化成员变量!如果想要为多个值进行特化,可能会有些复杂,但我喜欢这种简单的方式。 - AndyG

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