IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    C++ exit-time destructors

    MaskRay发表于 2024-03-17 07:02:58
    love 0

    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:

    • Limited atexit guarantee: ISO C (up to C23) does not require morethan 32 registered functions, although most implementations support manymore.
    • Dynamic library unloading: __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_NODELETEflag, making a library unloadable.

    Thread storage durationvariables

    Objects with thread storage duration that have non-trivialdestructors will register those destructors using__cxa_thread_atexit during construction.

    When exit-timedestructors are undesired

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

    • Unnecessary overhead and complexity: Operating system kernels andmemory-constrained systems
    • Potential race conditions: Destructors might execute during threadtermination, while other threads still attempt to access theobject.

    Clang provides -Wexit-time-destructors (disabled bydefault) 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-timedestructors

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

    Pointer/referenceto a dynamically-allocated object

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

    1
    2
    3
    4
    5
    6
    struct A { int v; ~A(); };
    A &g = *new A; // or A *const g = new A;
    A &foo() {
    static A &a = *new A;
    return a; // or 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 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
    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 anempty destructor

    A common approach, as outlined in P1247, is to use a class templatewith 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() { /* do nothing, union member is not destroyed */ }
    };
    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 similarfunctionality. The absl version optimizes for trivially destructibletypes.

    Compileroptimization for no-op destructors

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

    1
    2
    struct C { C(); ~C() {} };
    void foo() { static 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 attribute

    Clang supports [[clang::no_destroy]] (alternative form:__attribute__((no_destroy))) to disable exit-timedestructors 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_destroyattribute would have been beneficial. I've filed a GCC feature request(PR114357) after I learnedthat GCC doesn't have the attribute.



沪ICP备19023445号-2号
友情链接