본문으로 건너뛰기
Folly Code Review · 77/89

folly::Indestructible — global lifetime 패턴

· Hawk · 4분 읽기

한 줄 요약: Indestructible<T>생성은 하되 영원히 destroy하지 않는 wrapper다. Meyers singleton이 static deinitialization order fiasco를 일으키는 자리에서 destructor를 건너뛰어 안전을 산다.

#동기 — Static deinitialization order fiasco

foo.cpp
Foo& GetFoo() {
static Foo instance;
return instance;
}
// bar.cpp
Bar::~Bar() {
GetFoo().Cleanup(); // process exit 시 Foo가 이미 destroy됐을 수 있음
}

C++ 표준은 번역 단위 사이의 static destructor 순서를 보장하지 않는다. process exit 시점에 Foo가 이미 destroy됐는데 Bar::~Bar()가 호출되면 destroyed object 접근 — undefined behavior.

해법은 셋이다.

  1. Meyers singleton + 명시적 ordering — 의존 객체가 항상 의존받는 객체를 먼저 만들도록 설계. 실수 잦음.
  2. 할당된 채로 둠new Foo를 leak. process exit 시 OS가 회수. destructor 호출 안 됨.
  3. Indestructible — 2를 깔끔하게 표현.
#include <folly/Indestructible.h>
Foo& GetFoo() {
static folly::Indestructible<Foo> instance;
return *instance;
}

instance 자체는 stack-like static storage에 있다. 그러나 ~Foo()가 호출되지 않는다. process 끝나면 OS가 메모리 회수.

#API

#include <folly/Indestructible.h>
class MyConfig {
public:
MyConfig() { /* 생성 */ }
// 일부러 destructor를 두지 않음 (또는 두더라도 호출 안 됨)
std::string GetValue(folly::StringPiece key) const;
};
folly::Indestructible<MyConfig> kConfig;
void Use() {
auto v = kConfig->GetValue("foo");
// 또는 (*kConfig).GetValue(...)
}
template <class T>
class Indestructible {
public:
template <class... Args>
constexpr explicit Indestructible(Args&&... args) {
new (&storage_) T(std::forward<Args>(args)...);
}
// 소멸자 — empty. T를 destroy하지 않음.
~Indestructible() {}
T& operator*() { return *reinterpret_cast<T*>(&storage_); }
T* operator->() { return reinterpret_cast<T*>(&storage_); }
const T& operator*() const { /* ... */ }
};

placement new로 T를 생성하지만 소멸자에서 아무것도 안 한다. memory는 Indestructible 객체와 함께 reclaim되지만 T::~T()는 호출 안 됨.

#메모리 영역 관점

Indestructible<T>은 사실 static 영역의 byte 슬롯을 arena처럼 쓴다. 객체 lifetime이 process 전 구간이므로 free가 의미 없는 영역이다.

Heap vs stack vs arena

이 그림의 arena와 본질이 같다 — 한 번 자리잡으면 process가 끝날 때 OS가 통째로 회수한다. heap에 두고 leak시키는 것과 차이는 어디서 reclaim하는가일 뿐이다.

#왜 destructor를 건너뛰는가

// 잘못된 의존 — Logger가 Mutex에 의존
namespace {
Mutex logMutex; // 1
Logger logger; // 2 (logMutex 사용)
}
// process exit
// ~Logger() 가 실행 — logMutex 사용
// ~logMutex() 가 그 후 실행
// 만약 reverse 순서로 deinit되면 Logger가 dead Mutex 접근

Meyers singleton과 같은 문제. 한 번 만들고 영원히 살려두면 이 함정이 사라진다. process exit 시 모든 static destructor가 안 호출되는 게 오히려 안전한 사례.

folly::Indestructible<Mutex> logMutex;
folly::Indestructible<Logger> logger{*logMutex};
// 둘 다 destroy되지 않음 — 의존 그래프 신경 안 써도 됨

#비교 — leak vs Indestructible

// 1. raw leak
Foo* g_foo = nullptr;
Foo& GetFoo() {
static std::once_flag flag;
std::call_once(flag, [] { g_foo = new Foo; });
return *g_foo;
}
// 2. Indestructible
Foo& GetFoo() {
static folly::Indestructible<Foo> g_foo;
return *g_foo;
}
항목raw leakIndestructible
memoryheapstatic storage
initonce_flag 명시static init magic
코드길음한 줄
sanitizer leak 검출잡힘 (suppress 필요)잡히지 않음 (storage가 static)

Indestructible이 더 깔끔하다.

#constexpr 친화

// constexpr 생성자가 있는 T면 Indestructible도 constinit 가능
constinit folly::Indestructible<MyConfig> kConfig{/* args */};

constinit storage가 static initialization fiasco를 더 줄인다. 모든 의존이 compile-time 결정된다.

#std와의 비교

항목std (없음)folly::Indestructibleabsl::NoDestructor
도입N/A수년 전2020
APIN/AIndestructible<T>NoDestructor<T>
constexpr 생성자N/A지원지원
operator*N/A있음있음

Abseil이 같은 패턴을 absl::NoDestructor<T>로 늦게 도입했다. 둘은 거의 동일. 이름이 의도를 더 명확히 한다는 점에서 NoDestructor 명명이 낫다는 의견도 있다.

#Meyers singleton과 Indestructible

// Meyers
Foo& GetFooMeyers() {
static Foo instance; // ~Foo() 호출됨 → 순서 문제 risk
return instance;
}
// Indestructible
Foo& GetFooIndestructible() {
static folly::Indestructible<Foo> instance; // ~Foo() 호출 안 됨 → 안전
return *instance;
}

Meyers는 thread-safe init (C++11+)을 제공한다. Indestructible도 static initialization이므로 같은 보장. 차이는 destruction 시점.

대부분의 의존 객체는 process 생명주기 동안 살아있으면 충분. destructor가 의미 있게 할 일이 있는 객체만 일반 static. 예외 처리, 의도적 reset이 필요한 객체는 일반 static이 옳다.

#코드 리뷰 포인트

  • global mutex/logger/cache 가 일반 static T로 선언 → Indestructible로 바꿔 deinit 위험 제거.
  • Indestructible 내부 T가 RAII로 자원 release를 해야하는 타입 (file handle, network connection) → process exit 시 OS가 닫지 않는 자원이라면 leak. 그땐 일반 static.
  • Indestructible이 사용자 init 코드를 가지면 static init order 문제가 남음. 가능하면 default constructor.
  • ASan/LSan에서 false negative 우려 — leak이 OK인 거지 진짜 leak도 못 잡는 건 아님. 명시적 표현으로 의도 분명히.

#자주 보는 안티패턴

// 1. heavy 자원을 Indestructible로 보유
folly::Indestructible<std::vector<HeavyObject>> kCache{LoadCache()};
// → process exit 시 heap에 leak — OS가 회수하지만 leak detector noise
// 2. Indestructible을 thread_local
thread_local folly::Indestructible<Foo> kFoo;
// → thread 종료 시 storage 사라짐 — 의도와 맞나? 보통은 그냥 thread_local Foo가 OK
// 3. ~Indestructible() 실행 후 *kFoo 접근 (불가능하지만 의도 헷갈림)
// → Indestructible 자체는 trivially destructible이므로 함수 frame 끝에 사라짐
// global storage에 두는 게 일반 사용. local Indestructible은 어색.

#실전 — fbcode global registry

// log category registry
namespace folly {
LogCategoryRegistry& LoggerDB::get() {
static folly::Indestructible<LogCategoryRegistry> registry;
return *registry;
}
}

logger registry는 process 생명주기 동안 항상 있어야 하고 exit 시점에 임의 순서로 destroyed되면 안 됨. Indestructible이 표준 패턴.

#정리

  • Indestructible<T>영원히 살아있는 wrapper.
  • destructor 호출을 건너뛰어 static deinit order 문제를 회피.
  • Meyers singleton의 더 안전한 대안.
  • absl::NoDestructor<T>가 같은 패턴 (명명이 더 명확).
  • destructor가 정말 의미 있는 일을 해야 하면 일반 static 사용.

#다음 편

Part 18-03: MicroLock에서 1-byte lock primitive를 본다.

#관련 항목

Folly Code Review · 78 of 89

  1. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전