静态类成员未定义引用

216

有人能解释一下为什么以下代码不能编译吗?至少在g++4.2.4上不能。

更有趣的是,为什么将MEMBER强制转换为int后,代码就可以编译呢?

#include <vector>

class Foo {  
public:  
    static const int MEMBER = 1;  
};

int main(){  
    vector<int> v;  
    v.push_back( Foo::MEMBER );       // undefined reference to `Foo::MEMBER'
    v.push_back( (int) Foo::MEMBER ); // OK  
    return 0;
}

1
我编辑了问题,将代码缩进四个空格,而不是使用<pre><code> </code></pre>。这意味着尖括号不会被解释为HTML。 - Steve Jessop
您可以参考这个问题:https://dev59.com/BGQo5IYBdhLWcg3wI8nx - Aqeel Raza
自从C++17以来,就不需要额外的定义了,请参见下面的我的回答 - Quimby
9个回答

207
你需要在类定义之后实际地定义静态成员。试试这个:
class Foo { /* ... */ };

const int Foo::MEMBER;

int main() { /* ... */ }

这将消除未定义的引用。


3
不错,内联静态常数初始化会创建一个作用域整数常量,您无法获取该常量的地址;而向量采用引用参数。 - Evan Teran
14
这个回答只涉及问题的第一部分。第二部分更有趣:为什么添加NOP强制转换会使其工作而不需要外部声明? - Brent Bradburn
36
我刚花了一些时间才明白,如果类定义在头文件中,那么静态变量的分配应该在实现文件中进行,而不是在头文件中进行。 - shanet
1
@shanet:非常好的观点——我应该在我的回答中提到它! - Drew Hall
但如果我将其声明为const,那么我就无法更改该变量的值了吗? - Namratha
显示剩余2条评论

78
问题出在新的C++特性和你所尝试做的事情之间产生了有趣的冲突。首先,让我们来看一下`push_back`的签名:
void push_back(const T&)

它期望一个类型为T的对象引用。在旧的初始化系统下,这样的成员存在。例如,以下代码编译得很好:

#include <vector>

class Foo {
public:
    static const int MEMBER;
};

const int Foo::MEMBER = 1; 

int main(){
    std::vector<int> v;
    v.push_back( Foo::MEMBER );       // undefined reference to `Foo::MEMBER'
    v.push_back( (int) Foo::MEMBER ); // OK  
    return 0;
}

这是因为实际上有一个对象存储了该值。但是,如果你采用如上所示的新方法来指定静态常量成员,Foo::MEMBER 将不再是一个对象。它是一个常量,有点类似于:
#define MEMBER 1

但是不需要使用预处理器宏(并且具有类型安全性)。这意味着,期望引用的向量无法获取引用。


2
谢谢,那很有帮助...如果还没有的话,那可能符合https://dev59.com/1XI-5IYBdhLWcg3wJE0K的标准... - Andre Holzner
1
值得注意的是,MSVC可以接受非强制转换版本而不会出现任何问题。 - porges
4
这并不正确。当静态成员变量在某处_odr-used_时,仍然需要在内联初始化时定义它们。编译器优化可能会消除链接器错误,但这并不能改变这一点。在这种情况下,您的左值到右值转换(由于(int)强制转换)发生在具有常量完全可见性的翻译单元中,Foo::MEMBER不再是_odr-used_。这与第一个函数调用形成对比,在那里引用被传递并在其他地方进行评估。 - Lightness Races in Orbit
void push_back( const T& value ); 这个怎么样? const& 可以与 rvalue 绑定。 - Kostas

63
C++标准要求对于静态常量成员,如果其定义在某些情况下是必需的,则需要进行定义。例如,如果使用其地址,则需要进行定义并放置在命名空间中。当您显式转换常量时,会创建一个临时变量,并将其绑定到引用(根据标准中的特殊规则)。这是一个非常有趣的案例,值得提出问题,以便C++标准更改为具有相同的行为来处理您的常量成员!尽管以一种奇怪的方式,这可能被视为单目运算符“+”的合法用途。基本上,“单目+”的结果是一个右值,因此适用于将右值绑定到常量引用的规则,并且我们不使用静态常量成员的地址。
v.push_back( +Foo::MEMBER );

5
+1. 是的,对于一个类型为T的对象x来说,“(T)x”表达式可以用于绑定常量引用,而简单的“x”则不能,这确实很奇怪。我很喜欢你关于“一元+”的观察!谁会想到可怜的小“一元+”竟然有用处... :) - j_random_hacker
4
思考一般情况……在C++中是否有其他类型的对象具有以下属性:只有在定义后才能用作左值,但可以在未定义的情况下转换为右值? - j_random_hacker
好问题,目前我无法想出其他的例子。这可能只是因为委员会大多数时候只是重新使用现有的语法。 - Richard Corden
@RichardCorden:一元加号是如何解决这个问题的? - Blood-HaZaRd
1
@Blood-HaZaRd:在引入右值引用之前,push_back 的唯一重载是一个 const &。直接使用成员会导致成员被绑定到该引用上,这需要它有一个地址。然而,添加 + 创建了一个具有成员值的临时对象。引用随后绑定到该临时对象,而不需要成员具有一个地址。 - Richard Corden

10

Aaa.h

class Aaa {

protected:

    static Aaa *defaultAaa;

};

Aaa.cpp

// You must define an actual variable in your program for the static members of the classes

static Aaa *Aaa::defaultAaa;

7
在C++17中,使用inline变量可以提供一种更简单的解决方案:
struct Foo{
    inline static int member;
};

这是关于“成员变量”的定义,不仅仅是声明。类似内联函数,不同翻译单元中的多个相同定义不会违反ODR。现在不再需要为定义选择一个最喜欢的 .cpp 文件了。

2

一些额外的信息:

C++允许将整数和枚举类型的const静态类型定义为类成员。但这实际上并不是一个定义,只是一个“初始化标记”。

您仍然应该在类外部编写成员的定义。

9.4.2/4-如果静态数据成员是const整数或const枚举类型,则其在类定义中的声明可以指定一个常量初始化器,该常量初始化器应为整数常量表达式(5.19)。在这种情况下,该成员可以出现在整数常量表达式中。如果在程序中使用了该成员,则必须仍在命名空间范围内定义该成员,并且命名空间范围定义不得包含初始化器。


1

使用C++11,上述操作在基本数据类型方面是可行的,例如

class Foo {
public:  
  static constexpr int MEMBER = 1;  
};
constexpr 部分创建一个静态的 表达式,而不是一个静态的 变量 - 它的行为就像一个非常简单的内联方法定义。但在模板类中使用 C 字符串 constexpr 时,这种方法可能会有些不稳定。

这对我来说非常重要,因为在switch语句中使用MEMBER需要"static const int MEMBER = 1;",而在向量中使用它需要外部声明,而你不能同时拥有两者。但是你在这里提供的表达式确实可以同时适用于两者,至少在我的编译器上是这样的。 - Ben Farmer
@BenFarmer:这种方法在C++17中不再需要类外定义(因为它是隐式内联的,就像变量在那个版本中一样)。 - Davis Herring

1

不知道为什么强制类型转换起作用,但是 Foo::MEMBER 直到第一次加载 Foo 时才被分配,由于你从未加载它,因此它从未被分配。如果你在某个地方有一个 Foo 的引用,它可能会起作用。


我认为你已经回答了自己的问题:强制类型转换之所以起作用是因为它创建了(临时)引用。 - Jaap Versteegh

0
关于第二个问题:push_ref采用引用作为参数,你无法拥有一个类/结构体的静态常量成员的引用。一旦调用static_cast,会创建一个临时变量。可以传递对该对象的引用,一切都正常运行。
至少我解决此问题的同事是这么说的。

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