一个显著的区别在于以下代码可以编译和链接:
template<typename>
struct is_pointer { };
template<typename T>
struct is_pointer<T*> {
enum { value = true };
};
void f(const bool &b) { }
int main() {
f(is_pointer<void*>::value);
}
以下代码无法正常工作(您会得到一个“undefined reference”错误,指向“value”):
template<typename>
struct is_pointer { };
template<typename T>
struct is_pointer<T*> {
static const bool value = true;
};
void f(const bool &b) { }
int main() {
f(is_pointer<void*>::value);
}
当然,如果不添加以下行,则无法正常工作:
template<typename T>
const bool is_pointer<T*>::value;
那是因为[class.static.data]/3(我强调):
如果一个非内联的非易失性const静态数据成员属于整数或枚举类型,它在类定义中的声明可以指定一个大括号或等号初始化程序,在其中每个初始化器子句都是常量表达式([expr.const])。 如果在程序中odr-used该成员,则仍应在命名空间范围内定义该成员([basic.def.odr]),并且命名空间范围定义不应包含初始化程序。[...]
换句话说,static const bool value = true;
是一个声明,而不是定义,您不能odr-usevalue
。
另一方面,根据[dcl.enum/1](我强调):
枚举是有命名常量的不同类型。
这些命名常量可以像上面的示例一样作为const引用。
作为一个附注,如果你在C++11/14中使用静态constexpr数据成员,类似的情况也适用:
template<typename T>
struct is_pointer<T*> { static constexpr bool value = true; };
这不太好用,我发现它们之间存在微妙的差异。我在这里得到了帮助,从答案中获得了一些不错的提示。引用标准可以更好地解释底层发生的事情。
请注意,像上面那样声明
static
constexpr
数据成员也是自C++17以来的定义。因此,您将不再需要定义它,并且将能够直接使用odr。
如评论中所提到的(感谢@Yakk的确认),我也试图解释上述“命名常量”是如何绑定到const引用的。
[expr.const/3]引入了“整数常量表达式”,并通过说它被隐式转换为
prvalue来提到未作用域的
enum
。
[dcl.init.ref/5]和
[class.temporary/2]则完成了其余部分,因为它们规定了引用绑定和临时变量。
static
数据成员只有在定义时才存在于内存中,如果它们的地址被取出(通过指针或引用),则必须定义。当它节省内存时,这很棒,但当链接器抱怨你忘记了定义时,这很烦人 叹气。 - Matthieu M.