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

absl::string_view 함정 — dangling·c_str·임시 객체

· Hawk · 3분 읽기

#한 줄 요약

string_view는 가볍지만 non-owning이다. 소유권을 다른 객체에 의탁한다는 뜻이며, lifetime을 잘못 다루면 모든 잘못이 정적 분석으로 잡히지 않는다. 가장 자주 보이는 세 함정은 dangling reference, 임시 std::string 바인딩, null-terminator 가정이다.

#함정 1 — 임시 std::string 바인딩

가장 흔하다. 함수가 std::string을 반환하는 경우에 발생한다.

std::string GetUserName();
// 회피 — UB
absl::string_view name = GetUserName();
LOG(INFO) << name; // GetUserName()의 반환값은 이미 소멸됨

GetUserName()의 반환값은 임시 객체다. name이 그 임시의 내부 버퍼를 가리키는 순간, full expression이 끝나면서 임시는 파괴된다. name은 해제된 메모리를 가리킨다.

규칙: string_view로 함수 반환값을 받지 말 것. 함수가 view를 반환하는 경우에만 view로 받는다.

// Good — std::string으로 받는다
std::string name = GetUserName();
LOG(INFO) << name;
// Good — view를 반환하는 함수라면
absl::string_view ViewSomething();
absl::string_view v = ViewSomething();

리뷰 시점 패턴: string_view x = Func() 형태를 보면 Func의 반환 타입을 확인한다.

#함정 2 — 멤버 변수 보관

// 회피
class Logger {
public:
explicit Logger(absl::string_view prefix) : prefix_(prefix) {}
void Log(absl::string_view msg) { /* prefix_ 사용 */ }
private:
absl::string_view prefix_;
};
// 호출 측
std::string p = "[server] ";
Logger logger(p);
p = "[changed] "; // 원본이 변경되거나
// p가 스코프 밖으로 나가면 prefix_는 dangling

string_view를 멤버로 들면 객체의 lifetime이 외부 문자열의 lifetime에 묶인다. 사용자가 이 규약을 어기는 순간 UB가 발생한다.

// Good — 보관할 거면 복사한다
class Logger {
public:
explicit Logger(absl::string_view prefix) : prefix_(prefix) {}
// ...
private:
std::string prefix_; // 복사 보관
};

예외는 flyweight 패턴처럼 외부 영구 객체의 view를 의도적으로 들고 있는 경우다. 이때 의도를 주석으로 명시한다.

class Token {
// 원본 source의 lifetime > Token의 lifetime이 호출 규약으로 보장됨.
absl::string_view text_;
};

#함정 3 — c_str 변환

string_view에는 c_str()이 없다. 메모리 종단에 null이 있다는 보장이 없기 때문이다.

// substring view
absl::string_view full = "boom-shaka-laka";
absl::string_view first = full.substr(0, 4); // "boom"
// first.data()는 'b'를 가리키지만, first.data()[4]는 '-'이다

C API에 넘기려면 새 string을 만들어야 한다.

// 회피 — 컴파일 안 됨
open(view.c_str(), O_RDONLY);
// 회피 — view.data()는 null-terminated 아님
open(view.data(), O_RDONLY);
// Good — 변환 비용을 인지하고 알로케이션
std::string path(view);
open(path.c_str(), O_RDONLY);

이 변환은 alloc + 복사 비용이 든다. 짧은 경로라면 SSO 안에 들어가 alloc은 없지만 복사는 일어난다. C API 경계는 view의 약점이다. 가능하면 C++ API를 거치는 쪽으로 설계를 끌어간다.

#함정 4 — 비교 함수의 char* 트랩

absl::string_view sv = "boom";
const char* cs = sv.data(); // 위험: cs는 null-terminated 아닐 수 있음
strcmp(cs, "boom") == 0; // sv가 full string이면 우연히 동작

sv가 다른 view의 substring이면 strcmp는 종단을 넘어서 읽는다. 비교는 항상 view 비교 또는 absl 헬퍼로.

// Good
sv == "boom"
absl::EqualsIgnoreCase(sv, "boom")

#함정 5 — operator+의 함정

std::stringstring_view와의 +를 지원하지 않는다.

absl::string_view a = "hello";
absl::string_view b = " world";
// 회피 — 컴파일 안 됨
std::string s = a + b;
// 회피 — 부분 컴파일 (string + view는 지원)
std::string s2 = std::string(a) + b; // 변환 비용
// Good — StrCat
std::string s3 = absl::StrCat(a, b);

Part 4-03 — StrCat에서 StrCat이 왜 더 빠른지 본다.

#함정 6 — 잘못된 length 가정

생성자 두 종류를 혼동하면 데이터를 잘못 읽는다.

const char data[] = {'a', 'b', 'c', 0, 'd', 'e'}; // 6 bytes, 중간에 NUL
absl::string_view s1(data); // strlen 사용 → length=3 ("abc")
absl::string_view s2(data, 6); // 명시 length=6 ("abc\0de")

binary 데이터를 view로 다룰 때는 반드시 명시 length 생성자를 쓴다. strlen은 첫 NUL에서 끊는다.

#함정 7 — operator[]는 검사 없음

absl::string_view sv = "";
char c = sv[0]; // UB — bound check 없음

검사가 필요하면 sv.at(i)(예외)나 사전 size 검사로.

#코드 리뷰 패턴

이 함정들을 잡는 리뷰 룰은 단순하다.

  1. **string_view x = Func();**가 보이면 Func의 반환 타입을 본다. std::string이면 차단.
  2. 클래스 멤버 string_view는 lifetime 주석이 있는지 확인.
  3. view.data() 또는 view.c_str()이 보이면 C API로 흘러가는지 확인.
  4. binary 데이터 처리에 string_view(ptr) 단일 인자 생성자가 쓰이는지 확인.

#정적 분석 도구

clang의 -Wdangling-gsl 경고가 일부 패턴을 잡는다. [[clang::lifetimebound]] 어노테이션은 함수 인자의 lifetime이 반환값에 묶인다는 정보를 컴파일러에 준다.

// 명시: 반환된 view의 lifetime은 input의 lifetime에 묶인다
absl::string_view Trim(absl::string_view s [[clang::lifetimebound]]);
std::string GetSrc();
absl::string_view v = Trim(GetSrc()); // 컴파일러 경고

Abseil 자체도 일부 함수에 lifetimebound를 붙인다. 새 view 반환 함수를 만들 때 함께 붙이면 사용자 측 dangling을 줄인다.

#정리

  • string_view는 lifetime을 외부에 의탁하는 non-owning 참조.
  • 임시 std::string 바인딩, 멤버 변수 보관, C API c_str 변환이 3대 함정.
  • C API 경계에서는 std::string 변환 비용을 인지한다.
  • binary 데이터에는 명시 length 생성자만 쓴다.
  • [[clang::lifetimebound]]로 정적 분석 적용 범위를 넓힌다.

#다음 편

Part 4-03 — StrCat에서 string_view를 받는 가변 인자 문자열 연결 API를 본다.

#관련 항목

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