19

it's a pretty basic question but I don't understand why the code below does not compile on GCC 4.6.1. It does compile on VS 2008 with SP1:

#include <iostream>

class MyClass
{
public:
    const static int MinValue = -1000;
    const static int MaxValue = 1000;
};

void printValue(int i)
{
  std::cout << i << std::endl;
}

int main(int argc, char** argv)
{
  printValue(MyClass::MinValue);
  printValue(MyClass::MaxValue);
  printValue(argc < 42 ? MyClass::MinValue : MyClass::MaxValue); //This line gives the error
}

GCC says:

david@David-Laptop:~/temp$ g++ test.cpp
/tmp/ccN2b95G.o: In function `main':
test.cpp:(.text+0x54): undefined reference to `MyClass::MinValue'
test.cpp:(.text+0x5c): undefined reference to `MyClass::MaxValue'
collect2: ld returned 1 exit status

However, if I take out the third call to 'printValue' then it builds and runs properly. So it's something to do with the '?' operator... is it not valid to use it like this? Also, if I replace the 'argc < 42' with 'true' or 'false' it also builds fine.

Any ideas?!

David Williams
  • 753
  • 1
  • 4
  • 11
  • 2
    @VJovic: No - there must be a definition if it's *odr-used*, that is "unless it is an object that satisfies the requirements for appearing in a constant expression and the lvalue-to-rvalue conversion is immediately applied". That's the case for the function arguments, but not for the result of the conditional operator when the condition isn't a constant expression and the result is an lvalue. – Mike Seymour Jan 16 '12 at 12:46
  • 1
    @MikeSeymour: I think I have read some mails about odr-used and the ternary operators, would you mind expanding this in a proper answer ? I'd really like to understand the subtlety here. – Matthieu M. Jan 16 '12 at 12:52
  • @MatthieuM.: OK, but it might take a little while... – Mike Seymour Jan 16 '12 at 12:53

3 Answers3

18

According to the "One Definition Rule", a variable must have exactly one definition if is is odr-used. This is defined by the C++11 standard:

3.2/2 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 and the lvalue-to-rvalue conversion is immediately applied.

and the ODR itself:

3.2/3 Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program; no diagnostic required.

As function call arguments, they are not odr-used: they are constants with a value specified in their declaration, and so can appear in a constant expression; and they are passed by value, and so are immediately converted to rvalues.

This isn't the case when they are used in the conditional expression. Since both are lvalues referring to the same type, the result of the conditional expression will be an lvalue, according to one of the rather complicated rules defining the conditional operator:

5.16/4 If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category.

(This rule allows expressions like (a?b:c)=d.)

So the constants themselves are not immediately converted into rvalues, and the conditional expression can't appear in a constant expression due to the runtime condition; therefore, they are odr-used, and so need a definition.

As you note, changing the condition to a constant expression fixes the link error; so would changing the type of one constant. But the expression as it stands requires that they have a definition in one (and only one) translation unit:

const int MyClass::MinValue;
const int MyClass::MaxValue;
Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
  • Thanks Mike. Strangely enough, Clang 3.0 still accepts the code. – Matthieu M. Jan 16 '12 at 13:15
  • +1 Nice info. On a side note, gee, I'm gonna spend all my `+1` allowance on the answers to this question... I hope, there isn't going to be another informative answer, which I'll have to `+1` out of fairness. – lapk Jan 16 '12 at 13:20
  • You might add that violations of the one definition rule are "no diagnostic required" (a concept which the standard doesn't seem to define, but which leads inevitably to undefined behavior, since the standard "omits the description of any explicit definition of behavior") – James Kanze Jan 16 '12 at 13:45
  • 3
    Just to illustrate, changing the offending line to `printValue(argc < 42 ? (MyClass::MinValue + 0) : MyClass::MaxValue);` fixes it because one of the `?:` branches is no longer an lvalue. – Daniel Fischer Jan 16 '12 at 13:55
7

You need to define your static members outside the class declaration:

class MyClass
{
public:
    const static int MinValue;
    const static int MaxValue;
};


//implementation file
const int MyClass::MinValue = -1000;
const int MyClass::MaxValue = 1000;
Luchian Grigore
  • 253,575
  • 64
  • 457
  • 625
  • 1
    it doesn't explain why the last `printValue` is failing through. – greatwolf Jan 16 '12 at 12:25
  • 4
    Actually I thought that only applied to non-integral types? Or am I confusing it with something else. Also, why do the first two calls to printValue compile? – David Williams Jan 16 '12 at 12:26
  • @VictorT. it does, because you're trying to access a static member that is invalidly defined. – Luchian Grigore Jan 16 '12 at 12:27
  • I should have said - it does fix it. But I'm still onfused why the first two calls are ok. – David Williams Jan 16 '12 at 12:27
  • 1
    @PolyVox it could be that the compiler inlines them (it can since the class is visible) and not the third one. But that's just a guess. – Luchian Grigore Jan 16 '12 at 12:29
  • 2
    For my question about integral types I found some info in the IBM docs: "If a static data member is of const integral or const enumeration type, you may specify a constant initializer in the static data member's declaration. This constant initializer must be an integral constant expression. Note that the constant initializer is not a definition. **You still need to define the static member in an enclosing namespace.**" – David Williams Jan 16 '12 at 12:32
  • @R. Martinho Fernandes Thanks, perhaps that is why Visual Studio accepts it. – David Williams Jan 16 '12 at 12:34
  • @PolyVox: The function arguments are rvalues, so they only need the value of the constants; they don't require definitions. The (rather arcane) specification of the conditional operator says that its result is an lvalue if the second and third operands are lvalues of compatible types; forming an lvalue requires that there is a definition. (At least I think that's what's happening; this is rather a murky area). – Mike Seymour Jan 16 '12 at 12:34
  • @Mike Seymour Thanks, I've been doing C++ for years and it seems like I still have lots to learn ;-) – David Williams Jan 16 '12 at 12:37
  • It compiles on [GCC 4.5.1](http://ideone.com/eX26J). It might be that, for some reason, your compiler optimizes first two calls simply to inlined constants, since there is no run-time dependency. And it cannot in the 3rd case. However, it still should give you compilation error if you break rules, independently of whether it can or cannot optimize it. Looks like a bug to me. – lapk Jan 16 '12 at 12:39
  • @LuchianGrigore Oops, I guess, I was busy checking on [ideone](http://ideone.com) and missed your answer. You pointed to the possibility first, so you are getting an up-vote from me on the answer ;). – lapk Jan 16 '12 at 12:51
  • @AzzA: For the function call arguments, the compiler *must* perform that optimisation; the constants aren't odr-used, so they don't require definitions. In the conditional case, they *are* odr-used, so the definitions are required (although the standard says "no diagnostic required"). – Mike Seymour Jan 16 '12 at 12:52
  • @AzzA :)) that was not the intention, I was just confirming that your guess is valid :) – Luchian Grigore Jan 16 '12 at 12:52
  • 1
    Note you can still put the initializers in the class definition, and leave them off the member definitions. – aschepler Jan 16 '12 at 13:45
0

I'm going to go out on a limb and say this is probably a compiler bug with the version you're using. As AzzA noted in his comment, gcc-4.5.1 seems to build it fine as does gcc-4.3.4. I also just tested this with CLang 2.9 and that also accepts the code.

As an alternative, you can define your min/max values with enums rather than static const. This will save some typing:

#include <iostream>

class MyClass
{
public:
    enum { MinValue = -1000, MaxValue = 1000 };
};

void printValue(const int i)
{
  std::cout << i << std::endl;
}

int main(int argc, char** argv)
{
  printValue(MyClass::MinValue);
  printValue(MyClass::MaxValue);
  printValue(argc < 42 ? MyClass::MinValue : MyClass::MaxValue);
}
Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
greatwolf
  • 20,287
  • 13
  • 71
  • 105
  • +1 Yup, I checked on GCC 4.3.4 too 5 minutes ago. Quite interesting. – lapk Jan 16 '12 at 13:00
  • Initially I assumed it was a compiler bug but, given Mike Seymour's explanation, it seems it could be correct. Instead gcc-4.5.1 and 4.3.4 are accepting invalid code, meaning there was a bug which has now been fixed? – David Williams Jan 16 '12 at 13:13
  • @PolyVox Not providing a needed definition is "no diagnostic required" (aka undefined behavior), so whatever the compiler does is OK. – James Kanze Jan 16 '12 at 13:39
  • @PolyVox I guess this is why C++ has such bad rap. :P For something like this that *should* be simple but it isn't due to complex rules in the language standard. – greatwolf Jan 16 '12 at 13:40
  • For further reference, gcc-4.5.1 (20101208) rejects it here. – Daniel Fischer Jan 16 '12 at 13:52
  • 4.2.1 also rejects this. (I'm stuck with g++ 4.2.1 thanks to mis-feature of more modern versions of g++: GPL 3.0.) – David Hammen Jan 16 '12 at 15:02