Is there a way to force c++ compiler to not optimize out specific static objects in a static library?

Viewed 2225

(Only needs to work for gcc 5.4, if a general solution can't be found)

I have a generic factory that I use to construct objects based on some key (like a string representing a class name). The factory must allow classes to register that may not be known at construction time (so I can't simply register a list of classes explicitly).

As a means of registering these keys and their associated constructors, I have another 'RegisterInFactory' (templated) class. In each class's source file, I construct an object in an anonymous namespace corresponding to that class. This way, each class is automatically registered to the factory once the global objects are constructed. These objects never get used or referenced outside of doing this initial registration task.

However, when the code is compiled into a static library, when that library is linked into an executable, these static objects never get constructed, so the classes don't register to the factory, and the factory can't create anything.

I'm aware of the -Wl,--whole-archive -lfoo flag, which does include these global objects. But it also introduces a lot of 'multiple definition' errors. I'm aware that there's another flag that I can turn off the multiple definition errors, but I don't feel comfortable going without those errors. I'm aware of -u symbolName to turn off specific symbol names from these multiple definition errors (at least that's what I think it does). However, there are just too many of these redundant functions for that to be realistic (mostly from protobuf classes).

Is there any way to tell the compiler not to optimize those objects out, but only those objects so I can avoid the multiple definition issue? Is there another pattern I might be able to follow that fits within the constraints? (Particularly that I do not know at compile time what classes may be registered to the factory.)

Simplified Example code: Factory.h:

template<Base>
class Factory{
  ...
  template<Derived>
  class RegisterInFactory{
    RegisterInFactory(){
      instance().regInFactory(derivedConstructorFunctional);
    }
  };
};

In Derived.cpp:

namespace{ BaseFactory::RegisterInFactory<Derived> registerMe{"Derived"}; }

Final note: I've gotten lucky to some degree where without the linker flags, they still get included, but the only way that seems to happen is if the Derived class is 'sufficiently' complicated. Or maybe it's if I use the Derived class directly within the linked executable. I can't really tell why it's worked when it has.

1 Answers

The issue is not related to optimizations. Rather how linkers link symbols from static libraries.

However, when the code is compiled into a static library, when that library is linked into an executable, these static objects never get constructed, so the classes don't register to the factory, and the factory can't create anything.

That happens because nothing else refers to that registration variable. Hence, the linker is not pulling in the definition of the symbol from the archive.

To tell a Unix linker to keep that registration variable even if nothing else refers to it, use -Wl,--undefined=<symbol> compiler switch when linking to that static library:

-u symbol

--undefined=symbol

Force symbol to be entered in the output file as an undefined symbol. Doing this may, for example, trigger linking of additional modules from standard libraries. -u may be repeated with different option arguments to enter additional undefined symbols.

If that registration variable has "C" linkage, then <symbol> is the variable name.

For C++ linkage you will need to lookup the mangled name using nm --defined-only <object-file>. You may also need to put that variable into a named namespace, so that it has external linkage.


Example:

[max@supernova:~/src/test] $ cat mylib.cc
#include <cstdio>

namespace mylib {

struct Register
{
    Register() { std::printf("%s\n", __PRETTY_FUNCTION__); }
};

Register register_me;

}

[max@supernova:~/src/test] $ cat test.cc
#include <iostream>

int main() {
    std::cout << "Hello, world!\n";
}

[max@supernova:~/src/test] $ make
mkdir /home/max/src/test/debug
g++ -c -o /home/max/src/test/debug/test.o -MD -MP -std=gnu++14 -march=native -pthread -W{all,extra,error,inline} -ggdb -fmessage-length=0 -Og test.cc
g++ -c -o /home/max/src/test/debug/mylib.o -MD -MP -std=gnu++14 -march=native -pthread -W{all,extra,error,inline} -ggdb -fmessage-length=0 -Og mylib.cc
ar rcsT /home/max/src/test/debug/libmylib.a /home/max/src/test/debug/mylib.o
g++ -o /home/max/src/test/debug/test -ggdb -pthread /home/max/src/test/debug/test.o /home/max/src/test/debug/libmylib.a

[max@supernova:~/src/test] $ ./debug/test 
Hello, world! <-------- Missing output from mylib::register_me.

[max@supernova:~/src/test] $ nm --defined-only -C debug/mylib.o
0000000000000044 t _GLOBAL__sub_I__ZN5mylib11register_meE
0000000000000000 t __static_initialization_and_destruction_0(int, int)
0000000000000000 B mylib::register_me                        <-------- Need a mangled name for this.
0000000000000000 r mylib::Register::Register()::__PRETTY_FUNCTION__

[max@supernova:~/src/test] $ nm --defined-only debug/mylib.o
0000000000000044 t _GLOBAL__sub_I__ZN5mylib11register_meE
0000000000000000 t _Z41__static_initialization_and_destruction_0ii
0000000000000000 B _ZN5mylib11register_meE                   <-------- The mangled name for that.
0000000000000000 r _ZZN5mylib8RegisterC4EvE19__PRETTY_FUNCTION__

# Added -Wl,--undefined=_ZN5mylib11register_meE to Makefile.
[max@supernova:~/src/test] $ make 
g++ -o /home/max/src/test/debug/test -ggdb -pthread -Wl,--undefined=_ZN5mylib11register_meE /home/max/src/test/debug/test.o /home/max/src/test/debug/libmylib.a

[max@supernova:~/src/test] $ ./debug/test 
mylib::Register::Register() <-------- Output from mylib::register_me as expected.
Hello, world!
Related