I know that the C++ standard says (sec 9.4.2 paragraph 4) that a static member variable of integral or enum type can provide an initializer inside the class, but that this requires a definition of that member outside the class (in a compilation unit). I.e., you need to do something like this:
class A
{
public:
static const int X = 10;
};
// this is required by the standard
const int A::X;
I've seen (and I've seen it said other places) that some compilers will let you get away without the outside-of-class definition. This works on gcc 4.2.1 on OS X:
#include <iostream>
class A
{
public:
static const int X = 10;
};
int main(int argc, char** argv)
{
std::cout << A::X << std::endl;
return 0;
}
I recently encountered a bug where someone had done this, but they were using the member variable inside a templated function (std::max to be exact), and it would NOT compile, complaining about the undefined symbol A::X. I.e., this doesn't work:
#include <iostream>
#include <algorithm>
class A
{
public:
static const int X = 10;
};
int main(int argc, char** argv)
{
std::cout << std::max(1, A::X) << std::endl;
return 0;
}
Adding back in the outside-of-class definition makes it work.
This is sort of an academic question, but I'd like to know why this happens. Especially in relation to the fact that if we replace the static member variable with a static function (static const int X = 10; becomes static int X(), A::X becomes A::X()), then it will compile without the outside-of-class definition. The reason I mention templates is because std::max is templated, and other templated functions reproduce the same behavior. It may not be specifically related to templates, but I'd like to understand why it is that templates cause the behavior that they do. I assume this must have to do with the way that templates and static members get compiled/implemented?
PS - I posted some minimal code on github
It will compile without a definition.
The definition is needed at link time, if the static member variable is odr-used. (It wouldn't be odr-used if the compiler managed to substitute its actual value every time it was referenced. On the other hand, taking its address is sure to make it odr-used)
This is the complete rule (section 9.4.2 [class.static.data]):
If a non-volatile
const staticdata member is of integral or enumeration type, its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression. Astaticdata member of literal type can be declared in the class definition with theconstexprspecifier; if so, its declaration shall specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression. [ Note: In both these cases, the member may appear in constant expressions. — end note ] The member shall still be defined in a namespace scope if it is odr-used in the program and the namespace scope definition shall not contain an initializer.
and from section 3.2 [basic.def.odr]:
A variable 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 and the lvalue-to-rvalue conversion is immediately applied.
The requirements for appearing in a constant expression ARE satisfied, so everything depends on whether it is used as an lvalue or rvalue.
std::max takes an lvalue reference, so there is not an immediate lvalue-to-rvalue conversion.
The only interaction with templates is that there could be multiple equivalent definitions and the linker will pick any one. In your case, there is no template, so multiple definitions would produce a "symbol multiply defined" type of error.
When you forget to provide a definition, both cases (member of template class and member of ordinary class) will give the same error: "undefined symbol".
Let's take your first example:
std::cout << A::X << std::endl;
Because A::X is of type const int, this calls cout.operator<<(int value). Notice that in this call int value is taken by value. I believe the compiler performs constant folding and just replaces the A::X with the value. However, it is not required to do this in C++03. However, in C++11 the rules have changed and this has become required by this quote: unless it is an object that satisfies the requirements for appearing in a constant expression and the lvalue-to-rvalue conversion is immediately applied. (As mentioned by Ben Voigt).
Now let's look at the definition for std::max(1, A::X):
template< class T >
const T& max( const T& a, const T& b );
The std::max function takes it's arguments by reference, in other words, the address of A::X has to be known in order to complete the call. The address of A::x must be known which requires a definition.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With