对静态常量 int 的未定义引用

今天我遇到了一个有趣的问题,看看这个简单的例子:

template <typename T>
void foo(const T & a) { /* code */ }


// This would also fail
// void foo(const int & a) { /* code */ }


class Bar
{
public:
static const int kConst = 1;
void func()
{
foo(kConst);           // This is the important line
}
};


int main()
{
Bar b;
b.func();
}

在编译时,我得到一个错误:

Undefined reference to 'Bar::kConst'

现在,我非常肯定这是因为 static const int没有在任何地方定义,这是有意为之的,因为根据我的理解,编译器应该能够在编译时进行替换,而不需要定义。但是,由于该函数接受 const int &参数,因此它似乎不进行替换,而是首选引用。我可以通过以下改变来解决这个问题:

foo(static_cast<int>(kConst));

我相信这会迫使编译器生成一个临时 int,然后传递一个对它的引用,它可以在编译时成功地做到这一点。

我想知道这是不是故意的,还是我对 GCC 期望太高,以至于无法处理这个案子?还是因为某些原因我不该这么做?

43172 次浏览

I think this artefact of C++ means that any time that Bar::kConst is referred to, its literal value is used instead.

This means that in practise there is no variable to make a reference point to.

You may have to do this:

void func()
{
int k = kConst;
foo(k);
}

It's intentional, 9.4.2/4 says:

If a static data member is of const integral or const enumeration type, its declaration in the class definition can specify a constant-initializer which shall be an integral constant expression (5.19) In that case, the member can appear in integral constant expressions. The member shall still be defined in a namespace scope if it is used in the program

When you pass the static data member by const reference, you "use" it, 3.2/2:

An expression is potentially evaluated unless it appears where an integral constant expression is required (see 5.19), is the operand of the sizeof operator (5.3.3), or is the operand of the typeid operator and the expression does not designate an lvalue of polymorphic class type (5.2.8). An object or non-overloaded function is used if its name appears in a potentially-evaluated expression.

So in fact, you "use" it when you pass it by value too, or in a static_cast. It's just that GCC has let you off the hook in one case but not the other.

[Edit: gcc is applying the rules from C++0x drafts: "A variable or non-overloaded function whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied.". The static cast performs lvalue-rvalue conversion immediately, so in C++0x it's not "used".]

The practical problem with the const reference is that foo is within its rights to take the address of its argument, and compare it for example with the address of the argument from another call, stored in a global. Since a static data member is a unique object, this means if you call foo(kConst) from two different TUs, then the address of the object passed must be the same in each case. AFAIK GCC can't arrange that unless the object is defined in one (and only one) TU.

OK, so in this case foo is a template, hence the definition is visible in all TUs, so perhaps the compiler could in theory rule out the risk that it does anything with the address. But in general you certainly shouldn't be taking addresses of or references to non-existent objects ;-)

g++ version 4.3.4 accepts this code (see this link). But g++ version 4.4.0 rejects it.

If you're writing static const variable with initializer inside class declaration it's just like as if you've written

class Bar
{
enum { kConst = 1 };
}

and GCC will treat it the same way, meaning that it does not have an address.

The correct code should be

class Bar
{
static const int kConst;
}
const int Bar::kConst = 1;

This is a really valid case. Especially because foo could be a function from the STL like std::count which takes a const T& as its third argument.

I spent much time trying to understand why the linker had problems with such a basic code.

The error message

Undefined reference to 'Bar::kConst'

tells us that the linker cannot find a symbol.

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 U Bar::kConst

We can see from the 'U' that Bar::kConst is undefined. Hence, when the linker tries to do its job, it has to find the symbol. But you only declare kConst and don't define it.

The solution in C++ is also to define it as follows:

template <typename T>
void foo(const T & a) { /* code */ }


class Bar
{
public:
static const int kConst = 1;
void func()
{
foo(kConst);           // This is the important line
}
};


const int Bar::kConst;       // Definition <--FIX


int main()
{
Bar b;
b.func();
}

Then, you can see that the compiler will put the definition in the generated object file:

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 R Bar::kConst

Now, you can see the 'R' saying that it is defined in the data section.

Simple trick: use + before the kConst passed down the function. This will prevent the constant from being taken a reference from, and this way the code will not generate a linker request to the constant object, but it will go on with the compiler-time constant value instead.

You can also replace it by a constexpr member function:

class Bar
{
static constexpr int kConst() { return 1; };
};

I experienced the same problem as mentioned by Cloderic (static const in a ternary operator: r = s ? kConst1 : kConst2), but it only complained after turning off compiler optimization (-O0 instead of -Os). Happened on gcc-none-eabi 4.8.5.

The proper way in C++17 would be to simply make the variable constexpr:

static constexpr int kConst = 1;

constexpr static data member are implicitly inline.