在类定义中定义静态常量整数成员

123

据我了解,C++允许在类内定义静态常量成员,只要它是整数类型。

那么为什么以下代码会导致链接器错误呢?

#include <algorithm>
#include <iostream>

class test
{
public:
    static const int N = 10;
};

int main()
{
    std::cout << test::N << "\n";
    std::min(9, test::N);
}

我得到的错误是:

test.cpp:(.text+0x130): undefined reference to `test::N'
collect2: ld returned 1 exit status

有趣的是,如果我注释掉对std::min的调用,代码将能够成功编译和链接(即使test :: N也在前一行中被引用)。

你有什么想法?

我的编译器是Linux上的gcc 4.4。


4
在Visual Studio 2010上运行正常。 - Puppy
4
此错误的解释可以在 https://gcc.gnu.org/wiki/VerboseDiagnostics#missing_static_const_definition 找到。 - Jonathan Wakely
1
这个问题明显显示了 C++ 对于“不要使用 #define 定义常量”的回答仍然很差劲。 - Johannes Overmann
1
@JohannesOvermann 在这方面,我想提到自 C++17 以来全局变量的内联使用 inline const int N = 10,据我所知,它仍然有一个由链接器定义的存储位置。关键字 inline 在此情况下也可以用于在类定义测试中提供静态变量_定义_。 - Wormer
1
我该如何在另一个类中使用静态常量整型变量? - Abdel Aleem
显示剩余2条评论
7个回答

78
我的理解是,C++允许在类内定义静态常量成员,只要它是整数类型。 你有点正确。您可以在类声明中初始化静态const整数,但那不是定义。 有趣的是,如果我注释掉对std::min的调用,代码可以编译和链接(即使test::N也在前一行引用)。 知道发生了什么吗? std::min通过const引用获取其参数。如果它通过值获取它们,您就不会有这个问题,但是由于您需要一个引用,因此还需要一个定义。 以下是章节/节: 9.4.2/4-如果静态数据成员是const积分或const枚举类型,则其在类定义中的声明可以指定常量初始化器,该初始化器应为积分常量表达式(5.19)。 在这种情况下,该成员可以出现在积分常量表达式中。 如果在程序中使用该成员,则仍必须在命名空间范围内定义该成员,并且命名空间范围定义不得包含初始化程序。 请参阅Chu的答案以获取可能的解决方法。

我明白了,那很有意思。在这种情况下,提供声明点的值与在定义点提供值之间有什么区别?哪一个是推荐的? - HighCommander4
我认为只要你从未实际“使用”变量,就可以不定义它。如果你只将其作为常量表达式的一部分使用,则该变量永远不会被使用。否则,除了能够在头文件中看到该值之外,似乎没有太大区别 - 这可能是你想要的,也可能不是。 - Edward Strange
2
简洁的答案是 static const x=1; 是一个 rvalue 但不是 lvalue。该值在编译时作为常量可用(您可以使用它来定义数组)。static const y; [没有初始化程序] 必须在 cpp 文件中定义,并且可以用作 rvalue 或 lvalue。 - Dale Wilson
2
如果他们能够扩展/改进这个功能就太好了。在我看来,初始化但未定义的对象应该与字面量一样处理。例如,我们可以将字面量5绑定到const int&。那么为什么不将OP的test::N视为相应的字面量呢? - Aaron McDaid
有趣的解释,谢谢!这意味着在C++中,静态常量整数仍然不能替代整数#define。枚举始终只是有符号整数,因此必须使用枚举类来表示各个常量。对我来说,将具有常量和已知值的常量声明退化为文字常量并以这种方式编译而不出问题是相当明显的。C++还有很长的路要走... - Johannes Overmann
事实上,引用的问题在于我们需要为对象提供存储空间。有趣的事实:1)启用优化后,对std::min(9, test::N)的调用会得到优雅的编译;然后我认为在test::N周围加上括号会使表达式成为右值,但并不简单,2)代码std::min(9, (test::N))无法编译;然后右值引用仍然是一个_引用_,所以4)std::min(9, reinterpret_cast<const int&&>(test::N))std::move(test::N)也不行;最后5)std::min(9, test::N+0)就解决了问题。查看 - Wormer

58

Bjarne Stroustrup的例子在他的C++ FAQ中表明你是正确的,只有在需要取地址时才需要定义。

class AE {
    // ...
public:
    static const int c6 = 7;
    static const int c7 = 31;
};

const int AE::c7;   // definition

int f()
{
    const int* p1 = &AE::c6;    // error: c6 not an lvalue
    const int* p2 = &AE::c7;    // ok
    // ...
}

他说:"只有在静态成员有类外定义时,才能获取其地址"。这表明否则是不起作用的。也许你的min函数在幕后以某种方式调用了地址。

2
std::min 通过引用传递其参数,这就是为什么需要定义的原因。 - Rakete1111
如果AE是一个模板类AE<class T>,而c7不是int而是T::size_type,我该如何编写定义呢?我在头文件中将值初始化为“-1”,但clang说未定义的值,我不知道该如何编写定义。 - Fabian
@Fabian 我正在旅行中,使用手机且有些忙碌...但我认为你的评论听起来最好写成一个新问题。 编写一个 MCVE,包括您收到的错误,可能还要加上gcc的输出。我敢打赌人们很快就会告诉您情况。 - HostileFork says dont trust SE
@HostileFork:在编写MCVE时,有时您会自己找到解决方案。对于我的情况,答案是template<class K, class V, class C> const typename AE<K,V,C>::KeyContainer::size_type AE<K,V,C>::c7;其中KeyContainer是std::vector<K>的typedef。必须列出所有模板参数并写出typename,因为它是一个相关类型。也许有人会发现这个评论有用。但是,现在我想知道如何将其导出到DLL中,因为模板类当然在头文件中。我需要导出c7吗? - Fabian

26

另一种做法,至少对于整数类型来说,是在类中将常量定义为枚举:

class test
{
public:
    enum { N = 10 };
};

2
这可能会解决问题。当N被用作min()的参数时,它将导致创建一个临时变量,而不是尝试引用一个假定存在的变量。 - Edward Strange
这样做的好处是可以将其设为私有。 - Agostino

14

不仅限于 int 类型。但你不能在类声明中定义该值。如果你有:

class classname
{
    public:
       static int const N;
}

如果在.h文件中,则必须具有以下内容:

int const classname::N = 10;

在 .cpp 文件中。


2
我知道你可以在类声明中声明任何类型的变量。我说过,我认为静态整数常量也可以在类声明中被定义。这不是事实吗?如果不是,为什么编译器没有在我尝试在类内部定义它的那一行给出错误?此外,为什么std::cout行不会导致链接错误,但std::min行会呢? - HighCommander4
不可以在类声明中定义静态成员,因为初始化会发出代码。与内联函数发出代码的情况不同,静态定义是全局唯一的。 - Amardeep AC9MF
@高指挥官4:您可以在类定义中为“静态常量”整数成员提供初始化程序。但是那仍然不定义该成员。有关详细信息,请参见Noah Roberts的答案。 - AnT stands with Russia

9
这里还有另一种解决问题的方法:
std::min(9, int(test::N));

我认为Crazy Eddie的回答正确地描述了问题存在的原因。

5
或者甚至是 std::min(9, +test::N); - Tomilov Anatoliy
这里有一个重要的问题:这样做是否最优?我不知道你们,但我跳过定义的主要吸引力在于它不应该占用任何内存和使用const static时没有额外开销。 - Opux

8

从C++11开始,您可以使用:

static constexpr int N = 10;

理论上仍需要在.cpp文件中定义常量,但只要不取N的地址,任何编译器实现都很难产生错误;)


如果您需要将值作为类型为'const int&'的参数传递,就像示例中一样,该怎么办? :-) - Wormer
很好,你并没有通过那种方式实例化 N,而是传递了一个临时的const引用。https://wandbox.org/permlink/JWeyXwrVRvsn9cBj - Carlo Wood
C++17 可能行,不过 C++14 就不行了,甚至 gcc 6.3.0 及其更低版本也无法支持 C++17 的标准特性。但还是谢谢提醒。 - Wormer
啊,是的,你说得对。我没有在wandbox上尝试过c++14。噢,那就是我所说的“这理论上仍然需要你定义常量”的部分。所以,你说得对,这不是“标准”的。 - Carlo Wood

3
C++允许在类内定义静态常量成员。但是,根据3.1 §2规定:声明是定义,除非它声明一个没有指定函数体的函数(8.4),它包含extern说明符(7.1.1)或链接说明符(7.5),并且没有初始化程序或函数体,在类定义中声明了一个静态数据成员(9.4),它是一个类名声明(9.1),它是一个opaque-enum-declaration(7.2),或者它是一个typedef声明(7.1.3),using-declaration(7.3.3),或using-directive(7.3.4)。请保留HTML标记。

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