5

I understand that static const members have to have out-ouf-class definition it they are odr-used. But the thing is, my program compiles and run just fine even without members definition.

Let's take a look at this example from C++ FAQ:

class AE
{
public:
    static const int c6 = 7;
    static const int c7 = 31;
};
const int AE::c7;   // definition

void f()
{
    const int* p1 = &AE::c6;    // error: c6 not an lvalue
    const int* p2 = &AE::c7;    // ok

    cout << *p1 << endl;
}

int main()
{
    f();

    const int* p1 = &AE::c6; 
    std::cout << p1 << "\n";

    return 0;
}


//RESULT:
// 7
// 00007FF735E7ACE8

I see no error whatsoever. I use Visual Studio 2015, and this code compiles and runs just fine.

My question: Is this specific to msvc or there are some language changes that I'm not aware of?

UPDATE: This is not a duplicate, as I said at the very beginning: I do understand how this suppose to work, I don't understand why it doesn't work as it should.

11
  • 4
    Possible duplicate of Defining static const integer members in class definition
    – Yves
    Commented Feb 1, 2017 at 14:54
  • 2
    Do note that with optimizations turned on gcc also does not give an error on const int* p1 = &AE::c6; as it just optimizes it away Commented Feb 1, 2017 at 14:54
  • Windows c++ compiler is not good because it always try to help you nicely, meaning that it will do more implicitly. c6 is a declaration, not a definition.
    – Yves
    Commented Feb 1, 2017 at 14:56
  • 2
    @Petrof: First, you must understand that this is never a compiler error. It's a linker error if the result of compilation still uses the variable's address, but you have to actually use it, not just syntactically, or your odr-use may be removed by dead-code elimination.
    – Ben Voigt
    Commented Feb 1, 2017 at 15:17
  • 2
    This is not just an MSVS issue. See: coliru.stacked-crooked.com/a/03e908b91d12d0b5 Commented Feb 1, 2017 at 15:20

3 Answers 3

4

The program explicitely violates the one definition rule as you have stated in your question. But the standard does not require a diagnostic in this case. This is explicit in 3.2 One definition rule [basic.def.odr] §4 (emphasis mine)

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

and 1.4 Implementation compliance [intro.compliance] §2.3 says:

If a program contains a violation of a rule for which no diagnostic is required, this International Standard places no requirement on implementations with respect to that program.

That means that gcc is right to choke on that program because one rule has been violated. But it is not a bug when MSVC accepts it as a compiler extension(*) without even a warning because the standard places no requirement here.

We are in the compile time undefined behaviour named ill-formed program, no diagnostic required. A compiler implementation is free to do what it wants:

  • reject the program
  • automatically fix the error and generate the code as if the definition was present (for current case)
  • remove the offending line[s] if it makes sense and makes the program compilable - even if what is generated is not what the programmer expected
  • compile it into a program that ends in a run-time error
  • [add whatever you think of, but I have never seen a compiler able to hit my cat...]

(*) more exactly, it is a compiler extension if it is documented. Unfortunately, I currently have no MSVC compiler full documentation, so I can not say whether it is documented and is a conformant extension, or is not and is just hmm... one possible execution of the program

Here is a slight variation on OP's code demonstrating that the compiler did provide a definition:

#include <iostream>

class AE {
    // ...
public:
    static const int c6 = 7;
    static const int c7 = 31;
};
const int AE::c7;   // definition

int main()
{
    const int* p1 = &AE::c6;    // error: c6 not an lvalue
    const int* p2 = &AE::c7;    // ok
    // ...
    std::cout << *p1 << "(" << p1 << ") - " << *p2 << "(" << p2 << ")" << std::endl;
    return 0;
}

The output is (with an old MSVC 2008, debug mode to avoid as much optimization as possible):

7(00DF7800) - 31(00DF7804)

that is expected values and consecutive addresses

With same code, Clang 3.4 complains (as expected) at link time with

undefined reference to `AE::c6'
8
  • Quick cat or slow compilers? ;) Commented Feb 1, 2017 at 15:42
  • It's not really a compiler extension in MSVC, just that the compiler doesn't pass enough information forward to the linker to diagnose the error. Which is you point out, is entirely compliant behavior.
    – Ben Voigt
    Commented Feb 1, 2017 at 15:52
  • @BenVoigt: When I remember how linkers used to be in the old days, they take a bunch of translation units containing defined symbols, required symbols and mergeable ones. If MSVC can link that without error, I assume that it made the symbol mergeable, which is more or less the same as a definition. Commented Feb 1, 2017 at 16:02
  • @SergeBallesta: There's another category: no usage whatsoever. Given that the code in the question is dead code, this option seems the most likely.
    – Ben Voigt
    Commented Feb 1, 2017 at 16:03
  • @Petrof: Having these statements in main() would force the code to be used: const int* p1 = &AE::c6; std::cout << p1 << "\n";
    – Ben Voigt
    Commented Feb 1, 2017 at 16:11
1

It just so happens that your compiler has applied some optimisations that mean it doesn't require you to follow the One Definition Rule in this case.

However, the C++ standard still requires you to do so, so your program has undefined behaviour. The standard does not require a compiler to tell you when you violate this particular rule, so it's chosen not to. Indeed, it would have to put in extra effort to detect the problem.

4
  • After OP's edit, it cannot be because the variable has been optimized out. It has a valid address. So MSVC has decided to automatically and silently generate one because of the no diagnostic required. Commented Feb 1, 2017 at 16:25
  • @Serge: You can't really observe whether the address is valid until you attempt to dereference it, which the OP doesn't do. ;) Commented Feb 1, 2017 at 16:38
  • @Serge: Okay there you go :) Commented Feb 1, 2017 at 16:56
  • @LightnessRacesinOrbit: And when you do dereference the pointer, the compiler performs constant propagation and still doesn't access the variable. At least, clang was smart enough in my testing on rextester.com. Maybe if you did *reinterpret_cast<volatile int*>(reinterpret_cast<uintptr_t>(p1)), that should defeat constant propagation.
    – Ben Voigt
    Commented Feb 1, 2017 at 18:38
0

The C++ standard says ([basic.def.odr] 3.2 paragraph 2) "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 (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied."

Read this for further information

1
  • And his expression is never evaluated, because there are no calls to f(). Good catch.
    – Ben Voigt
    Commented Feb 1, 2017 at 15:56