在结构体/类中出现奇怪的未定义静态常量符号

33

我可能很累,也可能发生了一些奇怪的事情我没有意识到,因为下面的代码在链接时会导致 Foo::A 和 Foo::B 的未定义符号。尽管代码是从一个更大的项目中缩小的,但它展示了我正在研究的核心。

#include <algorithm>

struct Foo
{
    static const int A = 1;
    static const int B = 2;
};

int main()
{
    return std::min(Foo::A, Foo::B);
}

如果不使用std :: min函数模板,它可以正常工作,即只返回Foo :: A。在类/结构体外定义静态int也可以(在这个简单的情况下是全局的)。但是,一旦它们像这样在内部,链接器就找不到它们了。

有人能解释一下发生了什么吗?


1
可能是C++ - 在类定义中定义静态常量整数成员的重复问题。 - ks1322
6个回答

45

需要定义

您提供的代码是非标准的。虽然您可以在类中直接为const static int成员提供初始值,但仍需要提供单独的定义。这很奇怪,有点出人意料,但您需要像这样编写:

#include <algorithm>

struct Foo
{
    static const int A = 1;
    static const int B = 2;
};

const int Foo::A;
const int Foo::B;

int main()
{
    return std::min(Foo::A, Foo::B);
}

可以在类似于C++中的const和static修饰符的问题中找到标准的引用。

为什么有时候代码在没有定义的情况下“能够工作”?

至于为什么即使不提供定义也经常可以解决问题:如果您仅在常量表达式中使用这些成员,编译器将始终直接解析它们,并且不会留下链接器分辨率的访问。只有当您以某种无法由编译器直接处理的方式使用它时,链接器才会检测到符号未定义。我想这可能是Visual Studio编译器中的一个错误,但考虑到该错误的性质,我怀疑它永远不会被修复。

我不明白为什么您的源代码属于“链接器”类别,需要剖析std::min才能理解。 注意:当我在GCC在线测试时,它可以正常工作,错误没有被检测到。

另一种选择:使用枚举

另一个选择是使用枚举。 当您遇到不支持静态const int“内联”初始化程序的旧编译器(例如Visual Studio 6)时,此版本也很方便。 但请注意,对于std::min,使用枚举会遇到其他问题,并且需要使用显式实例化或转换,或者在一个命名的枚举中同时包含A和B,如Nawaz的答案所示:

struct Foo
{
    enum {A = 1};
    enum {B = 2};
};

int main()
{
    return std::min<int>(Foo::A, Foo::B);
}

标准

注意:即使Stroustrup C++ FAQ也犯了这个错误,并不像标准那样严格要求定义:

如果静态成员有一个类外定义,你才能取它的地址。

C++标准确实在9.4.2中要求必须提供定义:

C++03措辞:

如果程序中使用该成员并且命名空间作用域定义不包含初始化器,则该成员仍然应在名称空间范围内定义。

C++11对9.4.2的措辞略有不同:

3 如果该成员在程序中odr-used(3.2),则仍应在名称空间范围内定义。

3.2描述了odr-use:

3 变量x在表达式ex中作为可能评估的表达式出现的情况下是odr-used,除非x是满足出现在常量表达式(5.19)中的要求的对象,而ex是表达式e的潜在结果集中的元素,在其中对e应用左值到右值转换(4.1),或者e是一个废弃值表达式(第5条)。

4 每个程序必须包含在该程序中使用odr的每个非内联函数或变量的精确定义;不需要诊断。

我必须承认,我不确定C++11措辞的确切含义,因为我未能理解odr-use规则。


2
我没有意识到整型常量需要这样做。我经常看到这种代码,不一定是我写的。这是否意味着我们永远不应该像我展示的那样做,而总是在cpp文件中初始化静态常量? - murrekatt
奇怪。你确定100%吗?另一个非标准扩展,我甚至不知道它是非标准的?:( 但这真的错了吗?请参见http://www2.research.att.com/~bs/bs_faq2.html#in-class-那里的示例也使用未命名的枚举。 - Suma
我认为问题不同。std::min 不够灵活,无法接受这样的枚举值。请查看编译错误。命名枚举并不能解决问题。请参见 http://www.ideone.com/rY8y5。 - Suma
2
链接器错误可能是因为std::min函数通过引用接受它的参数。因此,编译器被迫记录使用了常量的地址(即使该函数被内联),以便需要该常量地址的每个翻译单元都获得相同的值(在C ++中,一个对象不能有多个地址)。 - Sylvain Defresne
1
@Suma:问题在于当编译器实例化std::min函数模板时,需要知道“命名”类型。我并没有说你不能一般定义无名枚举! - Nawaz
显示剩余4条评论

3
如果你只想要整数值,那么你也可以定义枚举

#include <algorithm>

struct Foo
{
    enum integrals { A = 1, B = 2} ;
};

int main()
{
    return std::min(Foo::A, Foo::B);
}

这已经足够了,在类的外部不需要声明!在线演示:http://www.ideone.com/oE9b5

1
感谢提供ideone链接 - 我之前不知道这个网站,很好的服务。 - Suma

2

您必须在类定义之外定义静态常量。

struct Foo {
    static const int A;
    static const int B;
};

const int Foo::A = 1;
const int Foo::B = 2;

2
如果是const,他也可以在内部声明。 - Nawaz

2
这里有很好的答案,但需要注意的是std::min() 的参数是引用,这就要求传入变量的地址,由于这些变量不会出现在编译单元的对象文件中,因此链接器无法解析它们的地址。

你可能在非优化构建中遇到了这个问题,对吗?

我敢打赌,如果你启用了gcc的优化,就不会出现这种情况。调用std::min()将被内联,引用也会消失。

此外,如果在调用std::min()之前将Foo::AFoo::B分配给两个局部变量,这个问题也会消失。

虽然这不是理想的解决方案,但如果你不拥有定义导致该问题的变量的代码,那么这是一个可以考虑的方法。


1

既然你基本上是将结构体用作命名空间,为什么不直接使用命名空间:

#include <algorithm>

namespace Foo
{
    const int A = 1;
    const int B = 2;
};

int main()
{
    return std::min(Foo::A, Foo::B);
}

1
谢谢,但是在我展示的例子中,我已经将事情从更大的东西中最小化了,其中常量在一个类内部。当然我可以做一些不同的事情,但我想知道那个具体的问题。 - murrekatt
重点是,如果你想避免在某个地方放置定义,那么你可以通过一个真正的命名空间来实现。 - John Ripley

0
另一个解决方案是将您的静态变量inline,这样它将在最终的翻译单元中可用,从而消除了未定义符号错误。
请注意,这仅适用于C++11之后的版本。

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