Destructor Case Studies
Best Practices for Safe and Efficient Teardown
Pete Isensee Facebook Reality Labs
1 Case Study: End Brace
}
2 Destructor Case Studies
Best Practices for Safe and Efficient Teardown
Pete Isensee Facebook Reality Labs
3 Slides and Code Presentation https://tinyurl.com/y3ehsaxt
Source Code https://godbolt.org/z/OUJp7F
4 Baseline C++17 Standard C++ Core Guidelines Standard libraries Visual Studio 2017 GCC 9.2 Clang libc++ 8.0.1
5 C++ Destructors Defined One With Deterministic No name Automatic No parameters Symmetric No return type Designed to Special Give last rites Member before object death Function
6 When Destructors Are Invoked
Scenario Destructor called Notes Named automatic Scope exit Called at } Statics and globals Program exit Reverse order of construction Thread locals Thread exit Reverse order of construction Free store delete expression Prior to memory being freed Array elements From last element to first Reverse order of construction STL container elements Container destroyed Unspecified order Temporary End of expression in which created Unless bound to ref/named obj Exception thrown Stack unwinding Reverse order of construction Explicit dtor t.~T() or p->~T(); Rare exit() For global & static objects only Plus atexit functions; no locals abort() No; immediate app exit No auto, global, or static dtors
7 Case Study: No Dtor Declared
// std::pair template
8 Implicit Destructors Not specified by programmer Public and inline Non-throwing unless base or members throw Implicitly declared as defaulted // As if you wrote: ~pair() noexcept = default; Implicit dtor appropriate for most objects
9 Recommendation Avoid specifying dtors whenever possible See Rule of Zero Only declare dtors for classes that require them Clearly conveys intent See Rule of Five and Rule of All or Nothing
10 Case Study: Performancegcc -O3 400 350 class Tribble { 300 std::string name; 250 200 int ID; 150 public: 100 asm asm lines 50 ~Tribble(); 0 }; w/ dtor no dtor int main() { std::vector
11 gcc -O3 Case Study: Default 450Dtor 400 350 class Tribble { 300 250 std::string name; 200 int ID; 150 100 public: lines asm 50 ~Tribble() = default; 0 }; default no dtor int main() { std::vector
12 Strong Recommendation The best destructor is no destructor Embrace implicit dtors Only declare dtors when they are required
class Tribble { std::string name; int ID; };
13 14 Case Study: Trivial Dtors
// std::bitset template
// no destructor declared };
15 Trivial Destructors Requirements Implicit (not declared) or defaulted (=default) Not virtual
Base classes have trivial dtors Implicit/Default Dtors Non-static members have trivial dtors Trivial destructors do nothing Trivial Compiler can optimize away! Dtors
16 Case Study: Extra Work ~WarpCore() { if (dilithiumChamber != nullptr) delete dilithiumChamber; dilithiumChamber = nullptr; matterAntimatterReactor.clear(); magneticField.reset(); plasmaConduitCount = 0; }
17 Avoid Redundant/Unnecessary Work
delete/free handle nullptr/NULL internally Avoid zeroing member pointers, handles, PODs Let member data clean up after itself
~WarpCore() { delete dilithiumChamber; }
18 Case Study: Public Funcs in Dtors ~WarpCore() { Shutdown(); // Is this OK? } void Shutdown() { delete dilithiumChamber; dilithiumChamber = nullptr; matterAntimatterReactor.clear(); magneticField.reset(); plasmaConduitCount = 0; } void Startup() { /* … */ } 19 Avoid Calling Public Funcs in Dtors Public functions must maintain class invariants Destructors don’t need to maintain invariants Avoid the overhead of unnecessary functions
~WarpCore() { delete dilithiumChamber; }
20 21 Case Study: Raw Resource class Phaser { HANDLE phaserEvent; // Other data public: ~Phaser() { if (phaserEvent) CloseHandle(phaserEvent); // Other cleanup code } };
22 Resource Wrapper struct ScopedHandle { HANDLE h; ScopedHandle(): h(INVALID_HANDLE_VALUE) {} ScopedHandle(HANDLE handle): h(handle) {} operator HANDLE() { return h; } ~ScopedHandle() { if (h != INVALID_HANDLE_VALUE) CloseHandle(h); } };
23 Wrap Raw Resources class Phaser { ScopedHandle phaserEvent; // Other data public: ~Phaser() { // Other cleanup code } }; Takeaway: Put any resource that needs to be released in its own object (RAII)
24 Case Study: Raw Pointers class Uhura { X* x; Y* y; public: Uhura() : x(new X),X) {y(new } Y) { } // Alert: leaky ~Uhura() { delete x; delete} y; } };
Dtors only called for fully constructed objs If ctor throws, object not fully constructed
25 Wrap Raw Pointers class Uhura { std::unique_ptr
Takeaway: Store only a single raw resource (pointer, handle, lock, etc.) in a class
26 27 Case Study: Raw Pointers, Part II class Chekov { std::vector
28 Wrap Raw Pointers, Part II class Chekov { std::vector
Takeaway: Don’t store owned pointers in containers
29 Case Study: Threads class Scotty { std::vector
30 Prefer Joining Threads class Scotty { std::vector
31 Joining Threads class joining_thread : public std::thread { public: ~joining_thread() { if (joinable()) join(); } void detach() = delete; }; Prefer joining_thread (or jthread C++20) to thread Related: don’t detach a thread 32 Case Study: Virtual Dtors
// std::memory_resource class memory_resource { public: virtual ~memory_resource() {} void* allocate(size_t bytes, size_t alignment); void deallocate(void* p, size_t bytes, size_t alignment); private: virtual void* do_allocate(/* as above */) = 0; virtual void do_deallocate(/* as above */) = 0; };
33 Virtual Destructors Guarantee that derived classes get cleaned up If delete on a Base* could ever point to a Derived* Rule of thumb: if virtual functions in class Destructor should be virtual Destructor should be public Idiom exception: mixins (e.g. old unary_function)
34 Case Study: Spock class Human : Ego, public virtual Id {}; class Vulcan: Katra, Kolinahr {}; class Spock : Human, Vulcan { Tricorder tricorder; Phaser phaser; };
{ Spock s; } // Order of destruction?
35 Order of Destruction Rule of Thumb: reverse order of construction Specifically: 1. Destructor body 2. Data members in reverse order of declaration 3. Direct non-virtual base classes in reverse order 4. Virtual base classes in reverse order
36 Destruction Order Example class Human : Ego, public virtual Id {}; class Vulcan: Katra, Kolinahr {}; class Spock : Human, Vulcan { 8 Ego Tricorder tricorder; Phaser phaser; Id }; Human 9 7 { Spock s; } 1 Katra Spock 6 Vulcan 3 Tricorder 4 Phaser 2 5 Kolinahr
37 38 Case Study: Virtual Funcs in Dtors class HelmsPerson { public: virtual ~HelmsPerson() { Release(); } private: virtual void Release() = 0; // pure virtual }; class Sulu : public HelmsPerson { … };
Takeaway: don’t call virtual functions from destructors (or constructors)
39 Case Study: Ignoring Exceptions
Teleporter::~Teleporter() { try { Stop(); pads.reset(); TeleporterManager::Destroy(); } catch (...) { } }
40 Destructors Should Never Throw Reasoning Dtors invoked when exception thrown, stack unwound If another exception is thrown: terminate()! Never allow an exception to exit a dtor Core Guideline: a destructor may not fail Try/catch(…) should still be rare
41 Indicate Dtor Doesn’t Throw
Teleporter::~Teleporter() noexcept { try { Stop(); pads.reset(); TeleporterManager::Destroy(); } catch (...) { } } CoreGuidelines best practice
42 43 Case Study: Custom Mem Objects class SpecialKirk { Kirk* k; public: SpecialKirk() { void* raw = myAlloc(sizeof(Kirk)); k = new (raw) Kirk; // placement new } ~SpecialKirk() noexcept { k->~Kirk(); // explicit destructor myFree(k); } }; 44 Explicit Destructors Destructors can be called directly Very powerful for custom memory scenarios Example uses Paired w/ placement new std::vector Custom allocators
45 Custom Allocators template
48 Case Study: Vector Dtor
~vector() { if (first != nullptr) { for (auto* p = first; p != last; ++p) { p->~T(); // run dtor on each element } a.deallocate(first, capacity()); } }
49 Side Trip: Destructor Traits
#include
~vector() { if (first != nullptr) { for (auto* p = first; p != last; ++p) { p->~T(); // run dtor on each element } a.deallocate(first, capacity()); } }
51 Fast Vector Destructor
~vector() { if (first != nullptr) { if constexpr (!is_trivially_destructible_v
52 Destructor Faves
// no destructor! { try { maythrow(); } catch(…){ } } noexcept = default; // but beware { closesocket(…); } noexc = delete; { free(p); } noexcept { assert(…); } noexcept { SetEvent(…); } noexcept { Log(…); } noexcept { lock_guard
53 Performance Destructors are called a LOT } They’re invisible in code Recommendations Streamline common dtors The best dtor is default/empty Inlining may be useful Measure/profile, update, rinse, repeat
54 References C++17 Standard http://www.open- std.org/jtc1/sc22/wg21/docs/papers/2017/n4659. pdf Core Guidelines https://github.com/isocpp/cppcoreguidelines Destructors https://en.cppreference.com/w/cpp/language/de structor
55 56 Recommended Practices
Follow the Principal of Minimalization Best dtor is no zero; avoid specifying whenever possible Only declare dtors when they are required Calling public functions in dtors is a red flag; avoid Avoid unnecessary/redundant work in dtors RAII is your friend Wrap raw resources in a class Don’t own more than a single raw resource Don’t store owned pointers in containers 57 Recommended Practices
Make dtor virtual iff delete Base* could be Derived* Don’t call virtual functions from a dtor (or ctor) Don’t let exceptions escape dtors; dtors must not fail Use explicit dtors cautiously, paired with placement new Destructor traits allow important optimizations Destructors: a great place to check invariants Optimize common destructors
58 If You Remember Only One Thing
The best destructor is no destructor
59 60 61 Slides and Code Presentation https://tinyurl.com/y3ehsaxt
Source Code https://godbolt.org/z/OUJp7F
62