folly::Indestructible — global lifetime 패턴
한 줄 요약:
Indestructible<T>는 생성은 하되 영원히 destroy하지 않는 wrapper다. Meyers singleton이 static deinitialization order fiasco를 일으키는 자리에서 destructor를 건너뛰어 안전을 산다.
#동기 — Static deinitialization order fiasco
Foo& GetFoo() { static Foo instance; return instance;}
// bar.cppBar::~Bar() { GetFoo().Cleanup(); // process exit 시 Foo가 이미 destroy됐을 수 있음}C++ 표준은 번역 단위 사이의 static destructor 순서를 보장하지 않는다. process exit 시점에 Foo가 이미 destroy됐는데 Bar::~Bar()가 호출되면 destroyed object 접근 — undefined behavior.
해법은 셋이다.
- Meyers singleton + 명시적 ordering — 의존 객체가 항상 의존받는 객체를 먼저 만들도록 설계. 실수 잦음.
- 할당된 채로 둠 —
new Foo를 leak. process exit 시 OS가 회수. destructor 호출 안 됨. - 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가 의미 없는 영역이다.
이 그림의 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 leakFoo* g_foo = nullptr;Foo& GetFoo() { static std::once_flag flag; std::call_once(flag, [] { g_foo = new Foo; }); return *g_foo;}
// 2. IndestructibleFoo& GetFoo() { static folly::Indestructible<Foo> g_foo; return *g_foo;}| 항목 | raw leak | Indestructible |
|---|---|---|
| memory | heap | static storage |
| init | once_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::Indestructible | absl::NoDestructor |
|---|---|---|---|
| 도입 | N/A | 수년 전 | 2020 |
| API | N/A | Indestructible<T> | NoDestructor<T> |
| constexpr 생성자 | N/A | 지원 | 지원 |
| operator* | N/A | 있음 | 있음 |
Abseil이 같은 패턴을 absl::NoDestructor<T>로 늦게 도입했다. 둘은 거의 동일. 이름이 의도를 더 명확히 한다는 점에서 NoDestructor 명명이 낫다는 의견도 있다.
#Meyers singleton과 Indestructible
// MeyersFoo& GetFooMeyers() { static Foo instance; // ~Foo() 호출됨 → 순서 문제 risk return instance;}
// IndestructibleFoo& 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_localthread_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 registrynamespace 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 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
- 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
- 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
- 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
- 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
- 6 Folly production validation 문화 — peta-scale에서 단련된 코드
- 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
- 8 folly::Promise·makeFuture — Future를 만드는 두 길
- 9 folly::SemiFuture vs Future — executor binding의 명시화
- 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
- 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
- 12 folly::Future retry·window·via — 제어 흐름 조합자
- 13 folly::fibers 분석 — M:N stackful coroutine
- 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
- 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
- 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
- 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
- 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
- 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
- 20 folly::IOBufQueue — chain의 push/pull 추상화
- 21 folly::io::Cursor·RWCursor — chain 위의 stream
- 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
- 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
- 24 folly::FBString 분석 — SSO + COW 구현
- 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
- 26 folly::StringPiece — string_view 호환 분석
- 27 folly Join·Split utilities — 문자열 분해와 결합
- 28 folly::to·tryTo — text↔num 변환 분석
- 29 folly Conv Customization — 사용자 타입 지원
- 30 folly Conv 성능 비교 — sprintf·stringstream 대비
- 31 folly::F14ValueMap vs std::unordered_map
- 32 folly::F14NodeMap — stable pointer가 필요할 때
- 33 folly::F14VectorMap — cache-friendly iteration
- 34 folly::F14FastMap — auto-select 동작
- 35 folly F14 internals — SIMD probing 메커니즘
- 36 folly::small_vector — inline storage 분석
- 37 folly::FixedString — compile-time string
- 38 folly::AtomicHashMap — lock-free read 분석
- 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
- 40 folly::EvictingCacheMap — LRU 구현 분석
- 41 folly::Synchronized — lock wrapper 패턴
- 42 folly::SharedMutex 분석
- 43 folly::Baton — one-shot wait 동기화
- 44 folly::RWSpinLock 분석
- 45 folly::PicoSpinLock — 1-byte spinlock
- 46 folly::ProducerConsumerQueue — SPSC 큐 분석
- 47 folly::MPMCQueue — multi-producer multi-consumer
- 48 folly::UnboundedQueue — 동적 크기 lock-free
- 49 folly::fibers::Channel — Go-like channel
- 50 folly::dynamic — JSON-like dynamic type 분석
- 51 folly JSON conversion — toJson·parseJson
- 52 folly dynamic ↔ struct — manual marshaling
- 53 folly dynamic Visitor pattern — type별 분기
- 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
- 55 folly::SingletonVault 분석 — 등록·소멸·의존성
- 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
- 57 folly::ExceptionWrapper — type-erased exception holder
- 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
- 59 folly::Optional vs std::optional
- 60 folly::Function vs std::function
- 61 folly::Lazy — 지연 초기화 wrapper
- 62 folly Meta 스타일 code review 패턴
- 63 folly anti-patterns — 잘못 쓰면 std보다 느림
- 64 folly vs std 선택 기준 분석
- 65 folly::coro 개요 — production C++20 코루틴 어댑터
- 66 folly::coro::Task — lazy single-shot 코루틴
- 67 folly::coro::AsyncGenerator — 비동기 스트림
- 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
- 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
- 70 folly::Expected — 결과 또는 오류
- 71 folly::Try — Future 결과 wrapper
- 72 folly::Try vs Expected 선택 기준
- 73 folly::Range — 일반 iterator pair
- 74 folly::Uri — URL 파서
- 75 folly Fingerprint64·128 — 분산 hash
- 76 folly SpookyHashV2 — fast non-crypto hash
- 77 folly::Init — main() 부트스트랩
- 78 folly::Indestructible — global lifetime 패턴
- 79 folly::MicroLock — 1-byte 락
- 80 folly::MicroSpinLock — 가장 좁은 spin lock
- 81 folly::format — legacy formatter 분석
- 82 folly::demangle — typeid 디망글링
- 83 folly::DynamicConverter — dynamic ↔ struct
- 84 folly::RecordIO — append-only 로그 파일 포맷
- 85 folly::io::Compression — zstd·lz4·snappy wrapper
- 86 folly::AsyncIO — io_uring·Linux AIO
- 87 folly::CancellationToken — 코루틴·Future 취소 전파
- 88 folly::observer — hot config의 atomic refresh
- 89 fbcode 패턴 모음 — folly 사용의 실전
관련 글
folly::SingletonVault 분석 — 등록·소멸·의존성
Part 12-02: SingletonVault — 모든 singleton의 통합 관리. 등록 순서, 의존성 그래프, eager/lazy 전략.
같은 시리즈에서 이어 읽기
folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
Part 12-01: Meyers singleton과 static 변수의 한계 — destruction order, fork safety, dependency 관리.
같은 시리즈에서 이어 읽기
folly::Singleton try_get·try_get_fast — TLS-cached 접근
Part 12-03: try_get vs try_get_fast — TLS 캐시로 hot-path singleton 접근을 nanosecond 수준으로.
같은 시리즈에서 이어 읽기