본문으로 건너뛰기
Abseil Code Review · 69/79

absl::Cleanup — 함수 종료 시 실행 보장

· Hawk · 4분 읽기

#한 줄 요약

absl::Cleanup함수 종료 시 lambda를 한 번 실행하는 RAII scope guard다. C API 호출 결과 정리, 임시 파일 삭제, 임시 상태 복구처럼 destructor를 갖지 않는 자원의 해제를 지역적으로 보장한다. gsl::final_action이나 folly의 ScopeGuard와 같은 계열이며 Abseil은 deduction guide와 [[nodiscard]]를 결합해 사용성을 끌어올렸다.

#동기

C API와 섞여 동작하는 코드에서는 RAII가 자연스럽지 않다.

// 회피 — 모든 return 분기에서 close 호출 반복
absl::Status Process(const char* path) {
int fd = ::open(path, O_RDONLY);
if (fd < 0) return absl::InternalError("open");
Header h;
if (!ReadHeader(fd, &h)) {
::close(fd);
return absl::DataLossError("header");
}
if (!Validate(h)) {
::close(fd);
return absl::InvalidArgumentError("header");
}
::close(fd);
return absl::OkStatus();
}

close 호출이 분기마다 반복된다. 분기를 하나 추가할 때마다 정리 코드도 함께 추가해야 한다. 빠뜨리면 leak이다.

Cleanup은 이 정리 책임을 진입 시점에 한 번 등록한다.

// Good
absl::Status Process(const char* path) {
int fd = ::open(path, O_RDONLY);
if (fd < 0) return absl::InternalError("open");
auto closer = absl::MakeCleanup([fd] { ::close(fd); });
Header h;
if (!ReadHeader(fd, &h)) return absl::DataLossError("header");
if (!Validate(h)) return absl::InvalidArgumentError("header");
return absl::OkStatus();
}

분기 추가에도 정리는 영향받지 않는다. 스코프를 벗어날 때 lambda가 한 번 실행된다.

#RAII 원리 — 무엇을 보장하는가

Cleanup은 결국 RAII / scope guard 패턴의 한 instance다. scope를 어느 경로로 빠져나가도 소멸자가 호출된다는 보장이 핵심.

RAII scope guard

정상 return, early return, exception throw 모두 동일하게 cleanup이 실행된다. C API에서 goto cleanup 패턴을 쓰던 자리를 그대로 대체할 수 있고, exception 안전성이 자동으로 따라온다.

scope 진입부터 종료까지의 시점별 동작은 다음과 같다.

Cleanup RAII scope flow

#API와 사용법

#include "absl/cleanup/cleanup.h"
// 기본 사용 — MakeCleanup factory
auto c = absl::MakeCleanup([&] { ::close(fd); });
// CTAD를 통한 더 짧은 형태 (C++17)
absl::Cleanup c = [&] { ::close(fd); };
// 명시 취소 — Invoke()는 호출되지 않음
std::move(c).Cancel();
// 명시 실행 — 즉시 호출 후 lambda 폐기
std::move(c).Invoke();

CancelInvokervalue 참조에서만 호출된다. 일반 변수에 묶인 상태에서는 호출할 수 없고 반드시 std::move로 소비해야 한다. lambda를 두 번 실행하거나 조용히 무시하는 실수를 막기 위한 설계다.

auto c = absl::MakeCleanup([] { LOG(INFO) << "exit"; });
c.Cancel(); // 컴파일 에러 — lvalue
std::move(c).Cancel(); // OK

#내부 구현

absl/cleanup/cleanup.h의 본문은 짧다.

namespace absl {
template <typename Callback>
class ABSL_MUST_USE_RESULT Cleanup {
public:
Cleanup(Callback callback)
: storage_(std::move(callback), /*engaged=*/true) {}
Cleanup(Cleanup&& other) = default;
void Cancel() && {
storage_.Disengage();
}
void Invoke() && {
storage_.Invoke();
}
~Cleanup() {
storage_.InvokeIfEngaged();
}
private:
cleanup_internal::Storage<Callback> storage_;
};
template <typename Callback>
Cleanup(Callback) -> Cleanup<std::decay_t<Callback>>;
template <typename... Args, typename Callback>
absl::Cleanup<Callback> MakeCleanup(Callback callback) {
return absl::Cleanup<Callback>(std::move(callback));
}
} // namespace absl

핵심 포인트는 세 가지다.

  • ABSL_MUST_USE_RESULT[[nodiscard]]. absl::MakeCleanup(...)의 반환값을 변수에 잡지 않으면 경고가 나온다. 이는 임시 객체를 만들고 즉시 소멸시켜 lambda를 호출하는 흔한 실수를 막는다.
  • deduction guide로 CTAD를 지원. absl::Cleanup c = lambda 한 줄로 끝난다.
  • Cancel/Invoke는 ref-qualified rvalue 멤버. 이중 호출 금지.

Storageengaged 플래그 + callback을 묶은 small wrapper다. 빈 lambda([]{...})에 대해서는 EBO(empty base optimization)로 1바이트만 차지한다.

#std / Folly와의 비교

항목stdfolly::ScopeGuardabsl::Cleanup
표준 포함×××
CTAD 한 줄SCOPE_EXIT 매크로absl::Cleanup c = ...
nodiscard 경고일부O
명시 canceldismiss()std::move(c).Cancel()
명시 invokestd::move(c).Invoke()
헤더 의존folly/ScopeGuard.habsl/cleanup/cleanup.h

folly의 SCOPE_EXIT 매크로는 익명 변수를 만들어 라인 끝에 lambda를 붙이는 형태다.

// folly
SCOPE_EXIT { ::close(fd); };
// abseil
absl::Cleanup _ = [&] { ::close(fd); };

매크로 회피와 명시적 변수 이름이라는 이점이 있는 대신 한 단어 더 길다. 명시 취소 API는 Abseil이 더 깔끔하다.

C++23부터 표준에 std::scope_exit이 들어올 예정이다. Abseil의 Cleanup은 그 사실상의 polyfill 역할을 한다.

#코드 리뷰 포인트

1. 임시 객체 금지

// 회피 — 임시 객체가 곧바로 destruct → lambda 즉시 실행
absl::MakeCleanup([&] { ::close(fd); });
// Good — 변수에 묶어 스코프 끝까지 살린다
auto c = absl::MakeCleanup([&] { ::close(fd); });

[[nodiscard]] 경고가 발견해 주지만 매크로·legacy 빌드에서 무시될 수 있다.

2. callback이 throw하지 않는지 확인

destructor가 호출하는 lambda이므로 예외를 던지면 std::terminate로 직행한다. noexcept lambda를 권장한다.

absl::Cleanup c = [&]() noexcept { ::close(fd); };

3. 캡처 lifetime

// 회피 — local 변수 ref가 cleanup보다 먼저 죽을 수 있다
auto MakeLogger() {
std::string name = "log";
return absl::MakeCleanup([&] { LOG(INFO) << name; }); // dangling
}
// Good — by value
auto MakeLogger() {
std::string name = "log";
return absl::MakeCleanup([n = std::move(name)] { LOG(INFO) << n; });
}

함수 밖으로 cleanup이 escape하면 & 캡처는 위험하다. 보통은 같은 함수 안에서만 쓰므로 &로 충분하다.

4. early return과의 조합

Cleanup은 early return 패턴의 동반자다. goto cleanup;을 쓰던 C 스타일 코드를 점진적으로 옮길 때 가장 먼저 손대는 도구다.

#자주 보는 안티패턴

상태 토글

// 회피 — 의미 모호
bool was_paused = scheduler.IsPaused();
scheduler.Pause();
absl::Cleanup _ = [&] { if (!was_paused) scheduler.Resume(); };

Cleanup 안에서 분기가 시작되면 destructor가 무엇을 할지 한눈에 안 들어온다. 차라리 작은 helper class를 만든다.

// Good
class ScopedPause {
public:
explicit ScopedPause(Scheduler* s) : s_(s), was_paused_(s->IsPaused()) {
if (!was_paused_) s_->Pause();
}
~ScopedPause() { if (!was_paused_) s_->Resume(); }
private:
Scheduler* s_;
bool was_paused_;
};

Cleanup단순 한 줄 정리에 가장 적합하다. 복잡한 상태 기계는 별도 RAII class가 옳다.

lambda 안에서 return 기대

absl::Cleanup _ = [&] {
if (some_cond) return absl::InternalError("..."); // 컴파일 에러 또는 무시
};

lambda 반환값은 void다. 정리 단계에서 발견한 에러를 함수 반환값에 실어 보낼 수 없다. 그런 동작이 필요하면 Cleanup이 아니다.

Cleanup 중첩으로 순서 의존

auto a = absl::MakeCleanup([&] { ::close(fd); });
auto b = absl::MakeCleanup([&] { Flush(fd); }); // b가 먼저 호출됨 (LIFO)

LIFO 순서를 명확히 알고 쓰면 문제 없지만, 등록 순서와 실행 순서가 반대라 리뷰어가 자주 헷갈린다. 가능하면 한 Cleanup에 묶거나 helper class로 분리한다.

#정리

  • absl::Cleanup은 lambda 기반 RAII scope guard.
  • [[nodiscard]] + rvalue-only Cancel/Invoke로 흔한 오용을 컴파일러가 잡아 준다.
  • C API 자원·임시 상태 복구·early return 분기에 가장 적합.
  • callback은 noexcept로, 캡처 lifetime에 주의.
  • 복잡한 상태 토글은 별도 RAII class가 낫다.

#다음 편

Part 14-02 — algorithm container 확장에서 absl::c_sort, c_find_if 등 container 전체를 받는 algorithm wrapper를 본다.

#관련 항목

Abseil Code Review · 70 of 79

  1. 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
  2. 2 Abseil 개요 — Google이 std를 보완한 이유
  3. 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
  4. 4 Abseil 빌드와 의존성 — Bazel vs CMake
  5. 5 Abseil LTS vs HEAD 릴리스 모델 분석
  6. 6 Abseil Versioning과 ABI 호환성 정책
  7. 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
  8. 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
  9. 9 absl::LogSeverity — 로그 레벨 타입
  10. 10 Abseil type_traits — negation·conjunction·void_t
  11. 11 Abseil Conformance·Policy 분석
  12. 12 Abseil Memory utilities 분석
  13. 13 Abseil raw_logging — heap-free 로깅
  14. 14 Abseil thread_annotations — clang TSA 통합
  15. 15 absl::Status — exception-free error handling
  16. 16 absl::StatusOr<T> — 값 또는 에러
  17. 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
  18. 18 absl::Status payload — 구조화된 에러 컨텍스트
  19. 19 absl::Status ↔ exception 변환 패턴
  20. 20 absl::string_view — non-owning 문자열 참조
  21. 21 absl::string_view 함정 — dangling·c_str·임시 객체
  22. 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
  23. 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
  24. 24 absl::StrJoin — 컨테이너 결합과 Formatter
  25. 25 absl::StrFormat — type-safe printf·FormatSpec
  26. 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
  27. 27 Abseil Escape — CEscape·HexEscape·Base64
  28. 28 absl::flat_hash_map — Swiss Table 기반 hash map
  29. 29 absl::flat_hash_set — set 버전 Swiss Table
  30. 30 absl::node_hash_map — stable pointer가 필요할 때
  31. 31 absl::btree_map — sorted·cache-friendly B-tree
  32. 32 absl::FixedArray — 런타임 크기 stack 배열
  33. 33 absl::InlinedVector — small buffer optimization
  34. 34 Abseil Swiss Table internals — control byte·SIMD probing
  35. 35 absl::Mutex — reader-writer·fairness·deadlock 검출
  36. 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
  37. 37 absl::Notification — once-only signal
  38. 38 absl::BlockingCounter·Barrier — 다중 thread 조율
  39. 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
  40. 40 absl::Time·Duration 분석 — 단단한 type
  41. 41 absl::Time Format·Parse
  42. 42 absl::CivilTime 분석
  43. 43 absl::time_zone 분석
  44. 44 absl::Time mocking — 테스트 친화 시간
  45. 45 absl::BitGen — 모던 난수 생성기
  46. 46 Abseil Random Distributions — Uniform·Exponential
  47. 47 Abseil Mocking Random — 테스트 결정성
  48. 48 Abseil Random Seeding·Entropy
  49. 49 absl::int128·uint128 분석
  50. 50 absl::bits — popcount·countl_zero
  51. 51 absl::optional vs std::optional
  52. 52 absl::variant 분석
  53. 53 absl::span 분석
  54. 54 absl::any 분석
  55. 55 absl::compare — three-way 비교
  56. 56 Abseil utility — apply·in_place
  57. 57 Abseil AbslHashValue 분석
  58. 58 Abseil HashState chaining
  59. 59 Abseil Custom hashable 구현
  60. 60 Abseil LOG·VLOG·CHECK 분석
  61. 61 Abseil LogSink 분석
  62. 62 Abseil LogEntry·structured logging
  63. 63 Abseil Stack trace·failure_signal_handler
  64. 64 ABSL_FLAG 정의 분석
  65. 65 Abseil ParseCommandLine 동작
  66. 66 Abseil Flag introspection·validation
  67. 67 Google 스타일의 Abseil 사용 패턴
  68. 68 Abseil 자주 보는 anti-pattern
  69. 69 std → absl 마이그레이션 전략
  70. 70 absl::Cleanup — 함수 종료 시 실행 보장
  71. 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
  72. 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
  73. 73 absl::bind_front와 Overload — 함수 객체 보조 도구
  74. 74 absl::Cord — 분산 시스템용 대용량 문자열
  75. 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
  76. 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
  77. 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
  78. 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
  79. 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피