In ISO C++ standards, [basic.start.term] specifies that:
Constructed objects ([dcl.init]) with static storage duration aredestroyed and functions registered with std::atexit are called as partof a call to std::exit ([support.start.term]). The call to std::exit issequenced before the destructions and the registered functions. [Note1: Returning from main invokes std::exit ([basic.start.main]). — endnote]
For example, consider the following code:
1 | struct A { ~A(); } a; |
The destructor for object a will be registered for execution atprogram termination.
In the Itanium C++ ABI, object construction registers the destructorwith __cxa_atexit
instead of atexit
for tworeasons:
__cxa_atexit
provides amechanism for handling destructors when dynamic libraries are unloadedvia dlclose
before program termination.Note: Some C library implementations, such as musl libc, treatdlclose
as a no-op. In glibc, a shared object with theDF_1_NODELETE
flag cannot be unloaded. Symbol lookupsinvolving STB_GNU_UNIQUE
set the DF_1_NODELETE
flag, making a library unloadable.
Objects with thread storage duration that have non-trivialdestructors will register those destructors using__cxa_thread_atexit
during construction.
Exit-time destructors for static and thread storage durationvariables can be undesired due to
Clang provides -Wexit-time-destructors
(disabled bydefault) to warn about exit-time destructors.
1 | % clang++ -c -Wexit-time-destructors g.cc |
Then, I will describe some approaches to disable exit-timedestructors.
Pointer/referenceto a dynamically-allocated objectWe can use a reference or pointer that refers to adynamically-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 thepointer/reference is part of the root set.
The primary downside is unnecessary pointer indirection whenaccessing the object. Additionally, this approach uses a mutable pointerin the data segment and requires a memory allocation.
1 | # %bb.2: // initializer |
A common approach, as outlined in P1247, is to use a class templatewith 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 similarfunctionality. The absl version optimizes for trivially destructibletypes.
Ideally, compilers should optimize out exit-time destructors forempty user-provided destructors:
1 | struct C { C(); ~C() {} }; |
LLVM has addressed this since2011. Its GlobalOpt pass eliminates __cxa_atexit
callsrelated to empty destructors, along with other global variableoptimizations.
In contrast, GCC has an open featurerequest for this optimization since 2005.
no_destroy
attributeClang supports [[clang::no_destroy]]
(alternative form:__attribute__((no_destroy))
) to disable exit-timedestructors 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 learnedthat GCC doesn't have the attribute.