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 struct pair { T1 a; T2 b; pair(): a(), b() {} pair(const pair&) = default; pair(pair&&) = default; pair(const T1& x, const T2& y) : a(x), b(y) {} // ... Destructor not specified };

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 { 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 v; v.emplace_back(); v.emplace_back(); }

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 v; v.emplace_back(); v.emplace_back(); }

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 class bitset { enum { Words = /* math on Bits and CHAR_BIT */ }; unsigned long long array[Words]; // array of POD public: constexpr bitset() noexcept; constexpr bitset(unsigned long long) noexcept;

// 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 x; std::unique_ptr y; public: Uhura(): x(new X), y(new Y) { } };

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 serviceRecord; public: ~Chekov() { for (auto* p : serviceRecord) delete p; } };

28 Wrap Raw Pointers, Part II class Chekov { std::vector> serviceRecord; public: // dtor no longer necessary };

Takeaway: Don’t store owned pointers in containers

29 Case Study: Threads class Scotty { std::vector pool; public: ~Scotty() { // necessary? for (auto& t : pool) { if (t.joinable()) t.join(); } } };

30 Prefer Joining Threads class Scotty { std::vector pool; public: // no dtor necessary };

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: class Human : Ego, public virtual Id {}; class : 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 struct MyAllocator : public std::allocator { T* allocate(size_t n) { auto* raw = myAlloc(n); if (raw == nullptr) throw std::bad_alloc(); return static_cast(raw); } void deallocate(T* raw, size_t) noexcept { myFree(raw); } }; 46 Custom Allocator Usage class SpecialKirk { Kirk* k; MyAllocator a; public: SpecialKirk() { auto* raw = a.allocate(sizeof(Kirk)); k = new (raw) Kirk; } ~SpecialKirk() { k->~Kirk(); a.deallocate(k, sizeof(Kirk)); } 47 Vector Internals template > class vector { T0 private: T* first; T1 T* last; T2 T* end; allocated A al; but }; unused slots here

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 class { std::string name; int armorClass; }; static_assert( is_destructible_v< Gorn >); static_assert( is_nothrow_destructible_v< Gorn >); static_assert(!is_trivially_destructible_v< Gorn >); static_assert(!has_virtual_destructor_v< Gorn >); 50 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()); } }

51 Fast Vector Destructor

~vector() { if (first != nullptr) { if constexpr (!is_trivially_destructible_v) { for (auto* p = first; p != last; ++p) { p->~T(); // destroy each element } } a.deallocate(first, capacity()); } }

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 l(m); { chkInvariants();} noexc /*modify shared data*/ { delete p; } noexcept } noexcept { InterlockedDecr();} noe { SecureZeroMemory(p,sz);}

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