9

I have recently discovered an annoying problem in some large program i am developing; i would like to understand how to fix it in a best way. I cut the code down to the following minimal example.

#include <iostream>
using std::cin;
using std::cout;

class MagicNumbers
{
public:
  static const int BIG = 100;
  static const int SMALL = 10;
};

int main()
{
  int choice;
  cout << "How much stuff do you want?\n";
  cin >> choice;
  int stuff = (choice < 20) ? MagicNumbers::SMALL : MagicNumbers::BIG; // PROBLEM!
  cout << "You got " << stuff << "\n";
  return 0;
}

I get link errors in gcc 4.1.2 when compiling with -O0 or -O1 but everything is OK when compiling with -O2 or -O3. It links well using MS Visual Studio 2005 regardless of optimization options.

test.cpp:(.text+0xab): undefined reference to `MagicNumbers::SMALL'

test.cpp:(.text+0xb3): undefined reference to `MagicNumbers::BIG'

I looked at the intermediate assembly code, and yes, the non-optimized code regarded SMALL and BIG as external int variables, while the optimized one used the actual numbers. Each of the following changes fixes the problem:

  • Use enum instead of int for constants: enum {SMALL = 10}

  • Cast the constant (any one) at each usage: (int)MagicNumbers::SMALL or (int)MagicNumbers::BIG or even MagicNumbers::SMALL + 0

  • Use a macro: #define SMALL 10

  • Not use the choice operator: if (choice < 20) stuff = MagicNumbers::SMALL; else stuff = MagicNumbers::BIG;

I like the first option best (however, it's not ideal because we actually use uint32_t instead of int for these constants, and enum is synonymous with int). But what i really want to ask is: whose bug is it?

Am i the one to blame for not understanding how static integral constants work?

Should i blame gcc and hope for a fix (or maybe the latest version already has a fix, or maybe there is an obscure command-line argument to make this work)?

Meanwhile, i just compile my code with optimizations, and it's a pain to debug :-O3

Community
  • 1
  • 1
anatolyg
  • 26,506
  • 9
  • 60
  • 134

8 Answers8

20

This is a known issue. The Standard is to blame or you for not providing a definition of the statics. Depending on your point of view :)

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
7

Static data members don't work like that in C++:

Static data members are not part of objects of a given class type; they are separate objects. As a result, the declaration of a static data member is not considered a definition. The data member is declared in class scope, but definition is performed at file scope. These static members have external linkage.

You're only declaring those constants, even though you're initializing them. You still have to define them at namespace scope:

class MagicNumbers
{
public:
    static const int BIG = 100;
    static const int SMALL = 10;
};

const int MagicNumbers::BIG;
const int MagicNumbers::SMALL;

That will get rid of the link errors.

Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
  • @John, I actually tested this, you know ;) – Frédéric Hamidi Nov 17 '10 at 21:57
  • @John, which part do you think is wrong? It's not how I would go about solving the problem, 'file scope' is kind of a misnomer, and it doesn't really say WHY use of ternary doesn't work but if/else does...but he is correct. – Edward Strange Nov 17 '10 at 22:00
  • @Frédéric: It is wrong. The testing only confirms a work-around for the fact that the compiler failed to recognise the special-case initialisation for a `static const' ordinal value. See [here](http://www.devx.com/tips/Tip/5602) for an very concise exposition by Danny Kalev. – Marcelo Cantos Nov 17 '10 at 22:01
  • @Frederic: I've no doubt that you did, and I know that your proposed solution *will* work. @Noah: the part I think is wrong is "You still have to define them at file scope" – John Dibling Nov 17 '10 at 22:02
  • 2
    Here we go: 9.4.2(4) [Classes] : "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 and the namespace scope definition shall not contain an initializer." – John Dibling Nov 17 '10 at 22:03
  • My interpretation: For `static const` (integral) types, you can put the definition with the declaration. The part about "shall be" simply means that if you do this, the value will still be in `MagicNumbers` scope – John Dibling Nov 17 '10 at 22:05
  • BTW, I'm not downvoting this answer – John Dibling Nov 17 '10 at 22:08
  • @John, @Marcelo, so that was a compiler bug after all, but since questioner did not provide valid C++, the garbage-in-garbage-out rule might also apply. Answer updated, and thank you for your corrections :) – Frédéric Hamidi Nov 17 '10 at 22:08
  • @Marcelo - I can't read the whole article but it appears to be in error: 'static const int MAX = 512; //definition' <- a false statement. See 9.4.2/4 – Edward Strange Nov 17 '10 at 22:11
  • @Noah: litb's answer here I think help's to clarify the situation – John Dibling Nov 17 '10 at 22:14
  • @John - that is true, but the text for the ternary operator seems to leave room for interpreting use of those names as /variables/ and not /constant expressions/. – Edward Strange Nov 17 '10 at 22:16
  • @john - yep, it does. I said the same thing without a known citation a half hour ago :p – Edward Strange Nov 17 '10 at 22:19
  • @John: I think you've misread the standard there. "The member shall still be defined in a namespace scope" means, "the programmer will type out a definition at namespace scope". It doesn't mean, "the compiler will magically insert a definition". – Steve Jessop Nov 18 '10 at 03:22
7

In spite of the conventional advice, I have found that static const int ... invariably gives me more headaches than good old enum { BIG = 100, SMALL = 10 };. And with C++11 providing strongly-typed enums, I now have even less cause to use static const int ....

Marcelo Cantos
  • 181,030
  • 38
  • 327
  • 365
3

Heh, according to the C++ standard, 9.4.2 (class.static.data):

If a static data member is of const literal 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. A static data member of literal type can be declared in the class definition with the constexpr specifier; 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 used in the program and the namespace scope definition shall not contain an initializer.

So the declaration is correct, but you still need to have a definition somewhere. I always thought you could skill the definition, but I suppose that isn't standard conforming.

MSN
  • 53,214
  • 7
  • 75
  • 105
  • it is standard conforming not to define them... sometimes, the term `used` is defined in the standard for a very specific meaning. See litb's answer for the reference to the relevant standard part and the defect report regarding this particular use. – Matthieu M. Nov 18 '10 at 07:59
  • @Matthieu, sweet! I love stackoverflow if only because litb knows more about C++ than I do. – MSN Nov 18 '10 at 17:00
  • the guy is truly impressive, I myself am fan of his templates "hack" :) – Matthieu M. Nov 20 '10 at 14:40
  • @Matthieu, which hack is that? – MSN Nov 21 '10 at 18:44
1

I'd be hard pressed to assert that it's anyone's bug.

Static const integrals given values at point of declaration are not variables, they're constant expressions. For there to be a variable you still need to define it.

The rules wrt the ternary operator are pretty absurdly complex, probably necessarily so, and actually doesn't really say anything about constant expressions, only rvalues; obviously the compiler thinks they should be variables unless optimization is cranked way up. I think it's free to interpret the expression either way (as a constant expression or as variable).

Edward Strange
  • 40,307
  • 7
  • 73
  • 125
1

I'm new to C++, but I think that your class declaration only declares that those static members exist, you still need to define them somewhere:

class MagicNumbers
{
public:
  static const int BIG;
  static const int SMALL;
};

const int MagicNumbers::BIG = 100;
const int MagicNumbers::SMALL = 10;

The higher optimisation levels probably include a level of static analysis thorough enough to determine that BIG and SMALL can be exchanged with their actual values and not to give them any actual storage (the semantics will be the same), so defining these variables in this circumstance would be redundant, hence it links OK.

dreamlax
  • 93,976
  • 29
  • 161
  • 209
0

You still need to allocate space for them somewhere:

class MagicNumbers
{
public:
  static const int BIG = 100;
  static const int SMALL = 10;
};
const int MagicNumbers::BIG;
const int MagicNumbers::SMALL;
cababunga
  • 3,090
  • 15
  • 23
0

Why are your magic numbers in a class?

namespace MagicNumbers {
    const int BIG = 100;
    const int SMALL = 10;
}

Problem solved without needing to worry about flaws in the C++ standard.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699