Reputation: 5807
I'm currently exploring a very tricky bug in Boost.Serialization related to singletons. For context: Boost 1.65 changed the implementation of the singleton breaking the is_destructed
notification which leads to segfaults on program exit or library unload. Boost 1.66 "fixed" this but leaks memory.
The singleton code (relevant to this question) boils down to this:
template<class T> struct singleton{
T& inst(){
static T t;
return t;
}
}
Using a static member function variable avoids the static init fiasco but still has the same problem with destruction.
However Finding C++ static initialization order problems shows code how to solve that: When the Ctor of A
uses B
then B
will be constructed first and hence destructed last. This is also stated in Destruction order of static objects in C++. (completion of the destructor happens in the reverse order of the completion of the constructor
)
So far so good. Boost.Serialization now uses multiple singletons of type extended_type_info_typeid<T>
to register some meta-data of the usertype T
in another singleton std::multiset<const bs::typeid_system::extended_type_info_typeid_0*,...>
. This is done by using the multiset
(assume all singletons from here) from the constructor of extended_type_info_typeid_0
. In the destructor of extended_type_info_typeid_0
the entry in the multiset
is removed.
This means we have exactly the situation described above and the multiset
should outlive the other instances.
This breaks down when using shared libraries. I have the following test case:
test_multi_singleton.cpp:
int f();
int g();
int main(int argc, char**){
// Make sure symbols are used
if(argc==8) return f();
if(argc==9) return g();
}
multi_singleton1.cpp:
#include <boost/serialization/extended_type_info_typeid.hpp>
int f(){
return 0 != boost::serialization::extended_type_info_typeid<int>::get_const_instance().get_key();
}
multi_singleton2.cpp:
#include <boost/serialization/extended_type_info_typeid.hpp>
int g(){
// Use different(!) type
return 0 != boost::serialization::extended_type_info_typeid<float>::get_const_instance().get_key();
}
Build with:
g++ multi_singleton1.cpp -lboost_serialization -fPIC -shared -olibmulti_singleton1.so
g++ multi_singleton2.cpp -lboost_serialization -fPIC -shared -olibmulti_singleton2.so
g++ test_multi_singleton.cpp -L. -lmulti_singleton1 -lmulti_singleton2
Run in valgrind:
valgrind ./a.out
One can see that this corrupts memory in Boost 1.65. The reason is the messed up order which I traced by hijacking and logging the ctor/dtor calls:
ctor 0x7f9f0aa074a0 std::multiset<const extended_type_info_typeid_0*>
ctor 0x7f9f0a7f63e0 extended_type_info_typeid<float>
ctor 0x7f9f0a7f64a0 std::multiset<const extended_type_info_typeid_0*>
ctor 0x7f9f0aa073e0 extended_type_info_typeid<int>
dtor 0x7f9f0aa073e0 extended_type_info_typeid<int>
dtor 0x7f9f0aa074a0 std::multiset<const extended_type_info_typeid_0*>
dtor 0x7f9f0a7f64a0 std::multiset<const extended_type_info_typeid_0*>
dtor 0x7f9f0a7f63e0 extended_type_info_typeid<float>
This is using GCC 6.4, but same with GCC 7.1. As you can see the 2 multisets are destroyed together and before the 2nd extended_type_info_typeid
.
Am I missing anything? Is this allowed by the C++ standard?
Upvotes: 4
Views: 616
Reputation: 12174
From basic.start.term/3:
If the completion of the constructor or dynamic initialization of an object with static storage duration strongly happens before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first.
Also:
For an object of array or class type, all subobjects of that object are destroyed before any block-scope object with static storage duration initialized during the construction of the subobjects is destroyed.
Upvotes: 1