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 viadlclose
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 | % clang++ -c -Wexit-time-destructors g.cc |
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 | struct A { int v; ~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 | # %bb.2: // initializer |
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 | template <class T> class no_destroy { |
libstdc++ employs a variant that uses a union member.
1 | struct A { ~A(); }; |
C++20 will support constexpr destructor:
1 | template <class T> union no_destroy { |
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 | struct C { 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.
- July 2018 discussion: https://discourse.llvm.org/t/rfc-suppress-c-static-destructor-registration/49128
- Patch: https://reviews.llvm.org/D50994 with follow-up https://reviews.llvm.org/D54344
- Documentation: https://clang.llvm.org/docs/AttributeReference.html#no-destroy
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.