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

absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR

· Hawk · 3분 읽기

한 줄 요약: RETURN_IF_ERRORASSIGN_OR_RETURN은 Status/StatusOr의 장황한 에러 전파를 한 줄로 줄인다. Google 사내에서는 사실상 표준 패턴이고, 매크로 expansion에 몇 가지 미묘한 트릭이 들어 있다.

#어떤 문제를 푸는가

absl::Status / absl::StatusOr<T>를 그대로 쓰면 코드의 절반이 에러 검사가 된다.

absl::Status Process(const std::string& path) {
absl::Status s1 = ValidatePath(path);
if (!s1.ok()) return s1;
auto r1 = LoadConfig(path);
if (!r1.ok()) return r1.status();
Config config = *std::move(r1);
auto r2 = CreateServer(config);
if (!r2.ok()) return r2.status();
Server server = *std::move(r2);
return RunServer(&server);
}

매크로로 단축.

absl::Status Process(const std::string& path) {
RETURN_IF_ERROR(ValidatePath(path));
ASSIGN_OR_RETURN(Config config, LoadConfig(path));
ASSIGN_OR_RETURN(Server server, CreateServer(config));
return RunServer(&server);
}

같은 의미. 노이즈가 4분의 1.

#RETURN_IF_ERROR

#define RETURN_IF_ERROR(expr) \
do { \
const absl::Status _absl_status_ = (expr); \
if (ABSL_PREDICT_FALSE(!_absl_status_.ok())) return _absl_status_; \
} while (0)

핵심 구조.

  1. do { ... } while (0) 패턴 — if-else 안에서 안전하게 쓸 수 있게.
  2. const Status _absl_status_ 임시 변수에 캐싱 — 인자가 함수 호출이면 두 번 평가되지 않음.
  3. ABSL_PREDICT_FALSE — ok 경로가 hot path임을 컴파일러에 힌트.

매크로 이름은 내부 식별자(_absl_status_)를 쓴다. 사용자 코드의 변수와 충돌하지 않게.

#사용 예시

RETURN_IF_ERROR(DoStep1());
RETURN_IF_ERROR(DoStep2());
if (cond) RETURN_IF_ERROR(DoIfCond()); // do-while로 안전
for (int i = 0; i < n; ++i) {
RETURN_IF_ERROR(DoOne(i));
}

#ASSIGN_OR_RETURN

StatusOr 버전. 값 추출과 에러 전파를 동시에.

// 기본 형태
ASSIGN_OR_RETURN(Config config, LoadConfig(path));
// 풀면
auto _absl_tmp_ = LoadConfig(path);
if (!_absl_tmp_.ok()) return _absl_tmp_.status();
Config config = *std::move(_absl_tmp_);

매크로 expansion의 진짜 모습은 더 복잡하다.

// 의사 코드
#define ASSIGN_OR_RETURN(lhs, rexpr) \
ASSIGN_OR_RETURN_IMPL(\
ABSL_INTERNAL_CONCAT(_status_or_, __LINE__), lhs, rexpr)
#define ASSIGN_OR_RETURN_IMPL(statusor, lhs, rexpr) \
auto statusor = (rexpr); \
if (ABSL_PREDICT_FALSE(!statusor.ok())) return statusor.status(); \
lhs = *std::move(statusor)

__LINE__을 활용해 같은 함수 안에서 여러 번 호출해도 변수 이름이 충돌하지 않게 한다.

#사용 예시

// 단순
ASSIGN_OR_RETURN(int n, ParseInt("42"));
// 객체 멤버 접근
ASSIGN_OR_RETURN(Config config, LoadConfig(path));
LOG(INFO) << "loaded: " << config.name();
// 기존 변수에 할당 — auto 없이
Config config;
ASSIGN_OR_RETURN(config, LoadConfig(path)); // 컴파일 에러
// 매크로가 `auto`로 가정하지 않음. 새 변수 선언이 보통.

#두 매크로의 조합

absl::Status Pipeline(const std::string& path) {
RETURN_IF_ERROR(ValidatePath(path));
ASSIGN_OR_RETURN(Config config, LoadConfig(path));
RETURN_IF_ERROR(config.Validate());
ASSIGN_OR_RETURN(Server server, CreateServer(config));
RETURN_IF_ERROR(server.Init());
return RunServer(&server);
}

선형 흐름이 그대로 드러난다. 에러 처리는 컴파일러가 자동으로.

#Abseil 외부 정의

흥미롭게도 ASSIGN_OR_RETURN은 Abseil 공식에 포함되지 않는다. Google 사내에는 있지만 오픈소스 Abseil에는 없다. 이유는 코드베이스마다 약간씩 다른 변형이 있기 때문이라고 알려져 있다.

다음과 같이 직접 정의하는 것이 일반적이다.

// 사용자 프로젝트의 status_macros.h
#define RETURN_IF_ERROR(expr) \
do { \
const absl::Status _absl_status_ = (expr); \
if (ABSL_PREDICT_FALSE(!_absl_status_.ok())) return _absl_status_; \
} while (0)
#define ASSIGN_OR_RETURN_IMPL(statusor, lhs, rexpr) \
auto statusor = (rexpr); \
if (ABSL_PREDICT_FALSE(!statusor.ok())) return statusor.status(); \
lhs = *std::move(statusor)
#define ASSIGN_OR_RETURN(lhs, rexpr) \
ASSIGN_OR_RETURN_IMPL(STATUS_MACROS_CONCAT(_statusor_, __COUNTER__), lhs, rexpr)
#define STATUS_MACROS_CONCAT_IMPL(x, y) x##y
#define STATUS_MACROS_CONCAT(x, y) STATUS_MACROS_CONCAT_IMPL(x, y)

gRPC, TensorFlow, Envoy 등 큰 프로젝트가 각자 정의해 쓴다. 표준화되지 않은 이유가 일종의 사회적 합의로 받아들여진다.

#추가 변형

#RETURN_IF_ERROR with prefix

에러 메시지에 컨텍스트 추가.

#define RETURN_IF_ERROR_PREFIX(expr, prefix) \
do { \
absl::Status _s = (expr); \
if (ABSL_PREDICT_FALSE(!_s.ok())) { \
return absl::Status(_s.code(), \
absl::StrCat(prefix, ": ", _s.message())); \
} \
} while (0)
// 사용
RETURN_IF_ERROR_PREFIX(LoadConfig(path), "loading config");
// 에러: "loading config: file not found"

#ASSIGN_OR_RETURN with stream

RETURN_IF_ERROR(expr) << "context"; 형태로 stream syntax를 지원하는 변형도 있다. 구현은 더 복잡해진다.

#코드 리뷰 포인트

// 회피 — 매크로 안의 인자가 부작용 있는 함수
RETURN_IF_ERROR(IncrementAndCheck());
// 보통은 안전 — _absl_status_에 캐싱됨. 한 번만 호출.
// 다만 매크로 정의에 따라 다를 수 있으니 확인.
// 회피 — 매크로 안의 인자가 너무 길어 가독성 떨어짐
ASSIGN_OR_RETURN(auto result,
SomeVeryLongFunctionName(argument1, argument2,
argument3, argument4));
// 한 줄에 모든 게 들어가 읽기 힘듦.
// Good — 호출을 분리
auto temp = SomeVeryLongFunctionName(arg1, arg2, arg3, arg4);
ASSIGN_OR_RETURN(auto result, std::move(temp));
// 또는 그냥 풀어서
auto r = SomeVeryLongFunctionName(...);
if (!r.ok()) return r.status();
auto result = *std::move(r);
// 회피 — 매크로를 if 조건 안에
if (RETURN_IF_ERROR(...)) { ... } // 컴파일 에러
// do-while로 감싸진 매크로는 expression이 아님.

리뷰에서:

  1. 에러 컨텍스트가 충분한가 — prefix 매크로 사용 검토.
  2. 매크로 안의 표현식이 너무 길지 않은가.
  3. 에러를 silent하게 swallow하지 않는가 — 매크로 사용은 그 자체로 전파.

#자주 보는 안티패턴

// 회피 — 매크로를 안 쓰고 매번 손으로
auto r = Op();
if (!r.ok()) return r.status();
auto v = *r;
// 자주 반복되면 매크로 사용 검토.
// Good
ASSIGN_OR_RETURN(auto v, Op());
// 회피 — 매크로 안에서 추가 처리 시도
ASSIGN_OR_RETURN(int x, Parse(s) + 1); // 어색
// `Parse(s) + 1`이 StatusOr<int>를 반환? operator+ 정의 안 됨.
// Good
ASSIGN_OR_RETURN(int x, Parse(s));
int y = x + 1;
// 회피 — 매크로 매번 작성
absl::Status MyFunc() {
auto s = ...;
if (!s.ok()) return s;
// 5번 더 반복
}
// 매번 손으로. 매크로로 옮길 것.
// 회피 — 매크로 안의 변수 이름과 사용자 변수 충돌
absl::Status _absl_status_ = ...; // 매크로 내부 이름과 충돌
RETURN_IF_ERROR(Op()); // 일부 구현에서 깨질 수 있음
// 매크로 내부 이름을 모르면 충돌 위험. _ 접두사 변수는 보통 reserved.

#std::expected의 monadic operations와 비교

매크로와 monadic 메서드는 같은 그림을 다르게 표현한 것이다.

Monadic StatusOr / Expected

C++23의 std::expected는 매크로 대신 monadic operations를 제공한다.

// std::expected — C++23
std::expected<int, MyError> r = ParseInt(s)
.transform([](int n) { return n * 2; })
.and_then([](int n) { return Validate(n); });

Abseil은 매크로 길을 택했다. 이유는 두 가지로 추정.

  1. C++14 호환 — lambda overhead, generic lambda 등.
  2. early return의 명시성RETURN_IF_ERROR는 코드에 return이 보임.

C++23 이후로는 expected의 monadic이 더 깔끔할 수 있다. 그러나 StatusOr를 쓰는 코드베이스에서는 매크로가 사실상 표준.

#정리

  • RETURN_IF_ERRORASSIGN_OR_RETURN은 Status/StatusOr 에러 전파의 표준 매크로.
  • 오픈소스 Abseil에는 포함되어 있지 않음. 프로젝트별로 정의하는 것이 일반적.
  • 내부적으로 do-while(0), __LINE__/__COUNTER__ 활용으로 안전성 확보.
  • ABSL_PREDICT_FALSE로 ok 경로 hot path 힌트.
  • 표현식이 길어지면 매크로 분리하거나 풀어 쓸 것.

#다음 편

Part 3-04에서 Status payload를 본다. 에러 메시지 외에 구조화된 컨텍스트를 어떻게 추가하는지, URL-based key 시스템이 어떻게 충돌을 방지하는지.

#관련 항목

Abseil Code Review · 17 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 회피