C++ exit-time destructors
2024-3-17 15:0:0 Author: maskray.me(查看原文) 阅读量:10 收藏

In ISO C++ standards, [basic.start.term] specifies that:

Constructed objects ([dcl.init]) with static storage duration are destroyed and functions registered with std::atexit are called as part of a call to std::exit ([support.start.term]). The call to std::exit is sequenced before the destructions and the registered functions. [Note 1: Returning from main invokes std​::​exit ([basic.start.main]). — end note]

For example, consider the following code:

1
struct A { ~A(); } a;

The destructor for object a will be registered for execution at program termination.

In the Itanium C++ ABI, object construction registers the destructor with __cxa_atexit instead of atexit for two reasons:

  • Limited atexit guarantee: ISO C (up to C23) does not require more than 32 registered functions, although most implementations support many more.
  • Dynamic library unloading: __cxa_atexit provides a mechanism for handling destructors when dynamic libraries are unloaded via dlclose before program termination.

Note: Some C library implementations, such as musl libc, treat dlclose as a no-op. In glibc, a shared object with the DF_1_NODELETE flag cannot be unloaded. Symbol lookups involving STB_GNU_UNIQUE set the DF_1_NODELETE flag, making a library unloadable.

Thread storage duration variables

Objects with thread storage duration that have non-trivial destructors will register those destructors using __cxa_thread_atexit during construction.

When exit-time destructors are undesired

Exit-time destructors for static and thread storage duration variables can be undesired due to

  • Unnecessary overhead and complexity: Operating system kernels and memory-constrained systems
  • Potential race conditions: Destructors might execute during thread termination, while other threads still attempt to access the object.

Clang provides -Wexit-time-destructors (disabled by default) to warn about exit-time destructors.

1
2
3
4
5
% clang++ -c -Wexit-time-destructors g.cc
g.cc:1:20: warning: declaration requires an exit-time destructor [-Wexit-time-destructors]
1 | struct A { ~A(); } a;
| ^
1 warning generated.

Disabling exit-time destructors

Then, I will describe some approaches to disable exit-time destructors.

Pointer/reference to a dynamically-allocated object

We can use a reference or pointer that refers to a dynamically-allocated object.

1
2
3
4
5
6
struct A { int v; ~A(); };
A &g = *new A;
A &foo() {
static A &a = *new A;
return a;
}

This approach prevents the destructor from running at program exit, as the dynamically allocated object will not be destroyed automatically. Note that this does not create a memory leak, since the pointer/reference is part of the root set.

The primary downside is unnecessary pointer indirection when accessing the object. Additionally, this approach uses a mutable pointer in the data segment and requires a memory allocation.

1
2
3
4
5
6
# %bb.2:                                 // initializer
movl $4, %edi
callq _Znwm@PLT
movq %rax, _ZZ3foovE1a(%rip) // store pointer of the heap-allocated object to _ZZ3foovE1a
...
movq _ZZ3foovE1a(%rip), %rax // load a pointer from _ZZ3foovE1a

Class template with an empty destructor

A common approach, as outlined in P1247, is to use a class template with an empty destructor to prevent exit-time destruction:

1
2
3
4
5
6
7
8
template <class T> class no_destroy {
alignas(T) std::byte data[sizeof(T)];
public:
template <class... Ts> no_destroy(Ts&&... ts) { new (data) T(std::forward<Ts>(ts)...); }
T &get() { return *reinterpret_cast<T *>(data); }
};

no_destroy<widget> my_widget;

libstdc++ employs a variant that uses a union member.

1
2
3
4
5
6
7
8
9
10
11
12
struct A { ~A(); };

namespace {
struct constant_init {
union { A obj; };
constexpr constant_init() : obj() { }
~constant_init() { }
};
constinit constant_init global;
}

A* get() { return &global.obj; }

C++20 will support constexpr destructor:

1
2
3
4
5
6
template <class T> union no_destroy {
template <typename... Ts>
explicit constexpr no_destroy(Ts&&... args) : obj(std::forward(args)...) {}
constexpr ~no_destroy() {}
T obj;
};

Libraries like absl::NoDestructor offer similar functionality. The absl version optimizes for trivially destructible types.

Compiler optimization for no-op destructors

Ideally, compilers should optimize out exit-time destructors for empty user-provided destructors:

1
2
struct C { C(); ~C() {} };
void foo() { static C c; }

LLVM has addressed this since 2011. Its GlobalOpt pass eliminates __cxa_atexit calls related to empty destructors, along with other global variable optimizations.

In contrast, GCC has an open feature request for this optimization since 2005.

no_destroy attribute

Clang supports [[clang::no_destroy]] (alternative form: __attribute__((no_destroy))) to disable exit-time destructors for variables of static or thread storage duration.

Standardization efforts for this attribute are underway P1247R0.

I recently encountered a scenario where the no_destroy attribute would have been beneficial. I've filed a GCC feature request (PR114357) after I learned that GCC doesn't have the attribute.


文章来源: https://maskray.me/blog/2024-03-17-c++-exit-time-destructors
如有侵权请联系:admin#unsafe.sh