我遇到了一些代码类似于以下的函数,使用了一些模板化的类A
:
template<typename X>
A<X>* get_A() {
static char storage[sizeof(A<X>)];
static A<X>* ptr = nullptr;
if(!ptr) {
new (reinterpret_cast<A<X>*>(storage)) A<X>();
ptr = reinterpret_cast<A<X>*>(storage);
}
return ptr;
}
我需要使这个初始化线程安全,所以我把它改为:
A<X>* get_A() {
static A<X> a;
return &a;
}
然而,这会导致段错误:get_A()
用于稍后被破坏的其他静态类的析构函数。 A
的复杂结构显然可以延长A
的寿命,超越任何其他物体的破坏,并且本身从未被破坏。我注意到https://en.cppreference.com/w/cpp/utility/program/exit说
如果一个函数本地(块范围)静态对象被销毁,然后从另一个静态对象的析构函数调用该函数,并且控制流通过该对象的定义(或者如果间接使用,则通过指针或引用),行为未定义。
由于静态存储和ptr不是'对象',我认为这个规则不会使第一个版本的未定义行为,但它确实阻止在函数内部构造static std::mutex
。因此,我的问题是:
get_A
是从具有静态生命周期的对象的析构函数调用的,那么单线程程序中的第一个版本在所有情况下都是合法的,或者它可能会强加未定义的行为?get_A
使用的情况下使此线程安全?我不希望为模板X
实例化的每个可能的A
都有初始化代码,因为A
实例化了许多不同的类型。除非事实证明这是唯一的好解决方案。我找到了解决方案:
A* get_A()
{
static typename std::aligned_storage<sizeof(A), alignof(A)>::type storage;
static A* ptr = new (reinterpret_cast<A*>(&storage)) A();
return ptr;
}
我已经将问题中使用的char
数组更改为std::aligned_storage
,以确保数组具有正确的对齐方式。在C ++ 17中,它可能需要std::launder
,但我使用的是C ++ 11。键入A
,函数当然可以像原始问题一样进行模板化,但我保持示例尽可能简单。
它仍然是一个黑客,但据我所知这是线程安全的,它允许静态对象的初始化,而不会破坏它而不会泄漏内存(当然,只要A没有自己的内存)。