5

I'm working with some legacy C++ code that is behaving in a way I don't understand. I'm using the Microsoft compiler but I've tried it with g++ (on Linux) as well—same behavior.

I have 4 files listed below. In essence, it's a registry that's keeping track of a list of members. If I compile all files and link the object files into one program, it shows the correct behavior: registry.memberRegistered is true:

>cl shell.cpp registry.cpp member.cpp
>shell.exe
1

So somehow the code in member.cpp gets executed (which I don't really understand, but OK).

However, what I want is to build a static library from registry.cpp and member.cpp, and link that against the executable built from shell.cpp. But when I do this, the code in member.cpp does not get executed and registry.memberRegistered is false:

>cl registry.cpp member.cpp  /c
>lib registry.obj member.obj -OUT:registry.lib
>cl shell.cpp registry.lib
>shell.exe
0

My questions: how come it works the first way and not the second and is there a way (e.g. compiler/linker options) to make it work with the second way?


registry.h:

class Registry {
public:

    static Registry& get_registry();
    bool memberRegistered;

private:
    Registry() {
        memberRegistered = false; 
    }
};

registry.cpp:

#include "registry.h"
Registry& Registry::get_registry() {
    static Registry registry;
    return registry;
}

member.cpp:

#include "registry.h"

int dummy() {
    Registry::get_registry().memberRegistered = true;
    return 0;
}
int x = dummy();

shell.cpp:

#include <iostream>
#include "registry.h"

class shell {
public:
    shell() {};
    void init() {
        std::cout << Registry::get_registry().memberRegistered;
    };
};
void main() {
    shell *cf = new shell;
    cf->init();
}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
MaartenB
  • 383
  • 1
  • 2
  • 12

3 Answers3

5

You have been hit by what is popularly known as static initialization order fiasco. The basics is that the order of initialization of static objects across translation units is unspecified. See this

The call here Registry::get_registry().memberRegistered; in "shell.cpp" may happen before the call here int x = dummy(); in "member.cpp"

EDIT:

Well, x isn't ODR-used. Therefore, the compiler is permitted not to evaluate int x = dummy(); before or after entering main(), or even at all.

Just a quote about it from CppReference (emphasis mine)

It is implementation-defined whether dynamic initialization happens-before the first statement of the main function (for statics) or the initial function of the thread (for thread-locals), or deferred to happen after.

If the initialization is deferred to happen after the first statement of main/thread function, it happens before the first odr-use of any variable with static/thread storage duration defined in the same translation unit as the variable to be initialized. If no variable or function is odr-used from a given translation unit, the non-local variables defined in that translation unit may never be initialized (this models the behavior of an on-demand dynamic library)...


The only way to get your program working as you want is to make sure x is ODR-used

shell.cpp

#include <iostream>
#include "registry.h"

class shell {
public:
    shell() {};
    void init() {
        std::cout << Registry::get_registry().memberRegistered;
    };
};

extern int x;   //or extern int dummy();

int main() {
    shell *cf = new shell;
    cf->init();
    int k = x;   //or dummy();
}

^ Now, your program should work as expected. :-)

Community
  • 1
  • 1
WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
  • Please can you confirm or deny that a static variable from a static library linked to two different DDLs can have two different values – Ed Heal Jul 19 '16 at 23:30
  • @EdHeal, Sorry - ["I can neither confirm nor deny ..."](http://www.imdb.com/title/tt2381249/quotes#qt2580242) ...your question looks crafty. :-) – WhiZTiM Jul 19 '16 at 23:50
  • @EdHeal ,,. Nonetheless, If you initialize the static variable in the static library using the *[On First Use Idiom](https://isocpp.org/wiki/faq/ctors#static-init-order-on-first-use)* the values should be the same on first access in both DLLs. I think any other way will be a UB, or better still Compiler dependent – WhiZTiM Jul 19 '16 at 23:58
  • 1
    @WhiZTiM I cannot see how SIOF applies here - the only statically initialised variable is `x`. The shell object `cf` is created within main, when static initialisation should be all over already. So what did I miss? – Aconcagua Jul 20 '16 at 00:04
  • @Aconcagua, you are right. However, "..when static initialization should be all over already.." - that's wrong, the compiler is permitted to initialize a static variable in main(), see my updated answer. C++ can be quirky :-) – WhiZTiM Jul 20 '16 at 03:01
  • "happens before the first odr-use of any variable" - as far as I understand this phrase, x (or alternatively y, if it existed within member.cpp) must be ODR-used *before* cf is created (more precisely: at latest before the call to `init()`) to guarantee the expected behaviour. Current version would now assure x being initialised at all (and thus setting memberRegistered to true), but still not necessarily before printing out the value of memberRegistered. – Aconcagua Jul 20 '16 at 04:53
  • By the way, cppreference is not 100% precise about that, the standard, however, is: C++14, 3.6.2.4: "before the first odr-use (3.2) of any *function or* variable" – Aconcagua Jul 20 '16 at 05:10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/117787/discussion-between-whiztim-and-aconcagua). – WhiZTiM Jul 20 '16 at 08:25
3

This is a result of the way linkers treat libraries: they pick and choose the objects that define symbols left undefined by other objects processed so far. This helps keep executable sizes smaller, but when a static initialization has side effects, it leads to the fishy behavior you've discovered: member.obj / member.o doesn't get linked in to the program at all, although its very existence would do something.

Using g++, you can use:

g++ shell.cpp -Wl,-whole-archive registry.a -Wl,-no-whole-archive -o shell

to force the linker to put all of your library in the program. There may be a similar option for MSVC.

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • This would force linking in the entire library and such force x to exist at least - according to WhiZTiM's answer, actual initialisation still might be delayed or even not happen at all. – Aconcagua Jul 20 '16 at 05:00
1

Thanks a lot for all the replies. Very helpful.

So both the solution proposed WhiZTiM (making x ODR-used) and aschepler (forcing linker to include the whole library) work for me. The latter has my preference since it doesn't require any changes to the code. However, there seems to be no MSVC equivalent for --whole-archive. In Visual Studio I managed to solve the problem as follows (I have a project for the registry static library, and one for the shell executable):

  1. In the shell project add a reference to the registry project;
  2. In the linker properties of the shell project under General set "Link Library Dependencies" and "Use Library Dependent Inputs" to "Yes".

If these options are set registry.memberRegistered is properly initialized. However, after studying the compiler/linker commands I concluded that setting these options results in VS simply passing the registry.obj and member.obj files to the linker, i.e.:

>cl /c member.cpp registry.cpp shell.cpp
>lib /OUT:registry.lib member.obj registry.obj
>link /OUT:shell.exe "registry.lib" shell.obj member.obj registry.obj
>shell.exe
1

To my mind, this is essentially the first approach to my original question. If you leave out registry.lib in the linker command it works fine as well. Anyway, for now, it's good enough for me.

I'm working with CMake so now I need to figure out how to adjust CMake settings to make sure that the object files get passed to the linker? Any thoughts?

toku-sa-n
  • 798
  • 1
  • 8
  • 27
MaartenB
  • 383
  • 1
  • 2
  • 12