Reputation: 5249
As far as I understand namespace scope static variables should have one copy in each compilation unit. So if I have a header file like this:
class BadLad {
public:
BadLad();
~BadLad();
};
static std::unique_ptr<int> sCount;
static BadLad sBadLad;
and badlad.cpp
#include "badlad.h"
BadLad::BadLad() {
if (!sCount) {
sCount.reset(new int(1));
std::cout<<"BadLad, reset count, "<<*sCount<<std::endl;
}
else {
++*sCount;
std::cout<<"BadLad, "<<*sCount<<std::endl;
}
}
BadLad::~BadLad() {
if (sCount && --*sCount == 0) {
std::cout<<"~BadLad, delete "<<*sCount<<std::endl;
delete(sCount.release());
}
else {
std::cout<<"~BadLad, "<<*sCount<<std::endl;
}
}
I expect sCount and sBadLad to be unique in each cpp file that includes badlad.h.
However, I found it's not the case in the following experiment:
libBadLad.so
.libPlugin.so
which links
libBadLad.so
, only plugin.cpp includes badlad.h
, so I expect
there is one copy of sCount
in libPlugin.so.sCount
in main.The main program looks like this:
#include <dlfcn.h>
int main() {
void* dll1 = dlopen("./libplugin.so", RTLD_LAZY);
dlclose(dll1);
void* dll2 = dlopen("./libplugin.so", RTLD_LAZY);
dlclose(dll2);
return 0;
}
When executing the main program, I can see the sCount
variable is first created and set to 1 before main is called, which is expected. But then after the first dlopen
is called, sCount
is incremented to 2, and subsequently decreased to 1 when dlclose
is called. The same happens to the second dlopen/dlclose.
So my questions is, why there is only one copy of sCount? Why the linker doesn't keep the copies separate (which I think is what most people expect)? It behaves the same if I link libPlugin.so to main directly instead of dlopen.
I'm running this on macOS with clang-4 (clang-900.0.39.2).
EDIT: please see the full source code in this repo.
Upvotes: 4
Views: 748
Reputation: 120059
(Iteration 2)
What happens in your case is very interesting and very unfortunate. Let's analyze it step by step.
libBadLad.so
. This shared library is thus loaded on program startup. Constructors of static objects are executed before main
.libplugin.so
. This shared library is then loaded, and constructors of static objects are executed.libBadLad.so
that libplugin.so
is linked against? Since the process already contains an image of libBadLad.so
, this shared library is not loaded the second time. libplugin.so
could just as well not link against it at all.libplugin.so
. There are two of them, sCount
and sBadLad
. Both are constructed, in order.sBadLad
has a user-defined non-inline constructor. It is not defined in libplugin.so
, so it is resolved against the already-loaded libBadLad.so
, which has this symbol defined.BadLad::BadLad
from libBadLad.so
is called.sCount
. This resolves to sCount
from libBadLad.so
, not sCount
from libplugin.so
, because the function itself is in libBadLad.so
. This is already initialised and is pointing to an int
that has the value of 1.sCount
from libplugin.so
sits quietly, being initialised to nullptr
. And the moral of the story is? Static variables are evil. Avoid.
Note the C++ standard has nothing to say about any of this, as it does not deal with dynamic loading.
However a similar effect can be reproduced without any dynaamic loaading.
// foo.cpp
#include "badlad.h"
// bar.cpp
#include "badlad.h"
int main () {}
Build and test:
# > g++ -o test foo.cpp bar.cpp badlad.cpp
./test
BadLad, reset count to, 1
BadLad, 2
BadLad, 3
~BadLad, 2
Segmentation fault
Why segmentation fault? This is our good old static initialisation order fiasco. The moral of the story? Static variables are evil.
Upvotes: 3