2
votes

Consider the following two translation units:

// foo.cpp
#include <iostream>

class Foo {
public:
  virtual ~Foo() = default;

  virtual void bar();

private:
  static int _baz;
};


static int f() {
  std::cout << "f called\n";
  return 42;
}

int Foo::_baz = f();

void Foo::bar() {
  std::cout << "Baz::bar called\n";
}

and

// main.cpp
#include <iostream>
int main() {
  std::cout << "main called\n";
}

When compiling both translation units into a single executable (e.g. with g++ -std=c++17 main.cpp foo.cpp, choice of optimization level or ordering of the two cpp files doesn't matter), the resulting executable prints

f called
main called

regardless of which of the three major compilers GCC, clang and MSVC was used to compile it. You can see the behaviour for yourself on wandbox.

My question is: Does the standard guarantee that Foo::_baz will be initialized (and thus f will be called) even if the entire class Foo isn't used in the program at all?

I believe this to be the case; my reasoning goes as follows:

According to [basic.start.dynamic]/4, Foo::_baz doesn't have to be initialized before the first statement of main is executed, but

if [the initialization] is deferred, it strongly happens before any non-initialization odr-use of any non-inline function or non-inline variable defined in the same translation unit as the variable to be initialized.

Here, "non-initialization odr-use" is defined as

[...] an odr-use ([basic.def.odr]) not caused directly or indirectly by the initialization of a non-local static or thread storage duration variable

in [basic.start.dynamic]/3. But as per [basic.def.odr]/7,

a virtual member function is odr-used if it is not pure.

from which I conclude that the definition of Foo::bar is a non-initialization odr-use of a non-inline function defined in the same translation unit as Foo::_baz, and thus Foo::_baz will be initialized.

What I find odd about this line of reasoning is that the deferred initialization of Foo::_baz would need to happen before the odr-use of Foo::bar, i.e. before Foo::bar is defined (wtf?!) since the definition alone is an odr-use. This makes me think my reasoning might be flawed.

So again: Does the standard guarantee that Foo::_baz will be initialized (and thus f will be called) even if nothing in its translation unit is ever used in the program at all? If yes, is there something we can say about when this will happen (given the weird ordering constraint that it has to happen before the virtual member function is defined), and if no, where's the error in my reasoning?

1
Same output if no virtual functions are involved. And the code you posted is not compilable. - user2100815
@NeilButterworth I've added the missing semicolon and missing include. It is interesting that there is the same output without the virtual function, I hadn't even tried that because surely that behaviour can't be guaranteed by the standard? - Corristo
AFAIK a compiler is fully entitled to force thie initialisation int Baz::_i = f();, but if the variable is not actually used anywhere, and there are no side-effects, then the optimiser could remove it. Proving either of those conditions would be extremely difficult, so I would always expect the initialisation to take place. Either way, it doesn't seem to have anything to do with virtual functions. - user2100815
@NeilButterworth The compiler is indeed allowed to perform static initialization instead of dynamic initialization here per eel.is/c++draft/basic.start#static-3 . But I'm asking whether any standard-conforming implementation is allowed to not call f. If my reasoning as written in the question is sound then f always has to be called, but I'm not sure I've missed something. - Corristo
AFAIK The "as-if" rule says that if the compiler can prove that a function has no side effects and its return value is not used, then it need not be called. Proving those things in a real program is very difficult, however. - user2100815

1 Answers

1
votes

The static initialization is not affected by virtual functions and the relation with dynamic initialization is about ordering, for constant initializers it happens as stated in [class.static.data]/2

[Note: Once the static data member has been defined, it exists even if no objects of its class have been created. ... — end note ]

However [basic.start.dynamic]/4

It is implementation-defined whether the dynamic initialization of a non-local non-inline variable with static storage duration is sequenced before the first statement of main or is deferred. If it is deferred, it strongly happens before any non-initialization odr-use of any non-inline function or non-inline variable defined in the same translation unit as the variable to be initialized.

Even if the implementation choose to defer the initialization, the standard mandate that it happens. and as a footnote of the previous clause:

A non-local variable with static storage duration having initialization with side effects is initialized in this case, even if it is not itself odr-used.