주간 팁 #5: 사라지는 객체의 함정
원래 TotW #5로 2012년 6월 26일 게시됨
2020년 6월 1일 업데이트됨
빠른 링크: abseil.io/tips/5
“무언가를 잃고 나서야 그것의 소중함을 알게 된다.”
- 신데렐라(Cinderella)
C++ 라이브러리를 올바르게 사용하려면 라이브러리와 언어 자체를 이해해야 할 때가 있습니다. 자, 다음 코드에 어떤 문제가 있을까요?
// 이렇게 하지 마세요!
std::string s1, s2;
...
const char* p1 = (s1 + s2).c_str(); // 피하세요!
const char* p2 = absl::StrCat(s1, s2).c_str(); // 피하세요!
s1 + s2
와 absl::StrCat(s1, s2)
는 임시 객체(이 경우 문자열)를 생성합니다. c_str()
멤버 함수는 임시 객체가 살아 있는 동안에만 유효한 데이터를 가리키는 포인터를 반환합니다. 그런데 이 임시 객체는 얼마나 오래 살아 있을까요? C++17 표준 [class.temporary]에 따르면, “임시 객체는 생성된 표현식을 (어휘적으로) 포함하는 전체 표현식이 평가되는 마지막 단계에서 소멸합니다.” 여기서 “전체 표현식”은 “다른 표현식의 하위 표현식이 아닌 표현식”을 의미합니다. 위의 예제에서, 할당 연산자의 오른쪽 표현식이 완료되자마자 임시 값이 소멸하며, c_str()
의 반환 값은 더 이상 유효하지 않은 포인터가 됩니다.
요약하자면? 세미콜론(종종 그보다 더 빨리)에 도달하면 임시 객체는 사라집니다. 이런 문제를 어떻게 피할 수 있을까요?
옵션 1: 전체 표현식이 끝나기 전에 임시 객체 사용 완료
// 안전한 코드 (다소 유치한 예시):
size_t len1 = strlen((s1 + s2).c_str());
size_t len2 = strlen(absl::StrCat(s1, s2).c_str());
옵션 2: 임시 객체 저장
이미 객체를 스택에 생성하고 있다면, 그 객체를 조금 더 오래 유지하는 것이 어떨까요? 이는 생각보다 저렴합니다. “반환 값 최적화(RVO)”와 이동语법(move semantics)을 통해 임시 객체는 “할당”될 변수에 바로 생성되며 복사가 일어나지 않습니다.
// 안전한 코드 (그리고 생각보다 효율적임):
std::string tmp_1 = s1 + s2;
std::string tmp_2 = absl::StrCat(s1, s2);
// tmp_1.c_str()와 tmp_2.c_str()는 안전합니다.
옵션 3: 임시 객체에 대한 참조 저장
C++17 표준 [class.temporary]: “참조에 바인딩된 임시 객체는 참조의 수명 동안 유지됩니다.”
반환 값 최적화 덕분에, 이는 객체 자체를 저장하는 것(옵션 2)보다 더 저렴하지 않으며, 혼란스럽거나 걱정을 유발할 수 있습니다(팁 #101 참조). 수명 연장을 필요로 하는 예외적인 경우는 주석을 통해 명확히 해야 합니다.
// 동등하게 안전:
const std::string& tmp_1 = s1 + s2;
const std::string& tmp_2 = absl::StrCat(s1, s2);
// tmp_1.c_str()와 tmp_2.c_str()는 안전합니다.
// 그러나 다음 동작은 매우 미묘합니다:
// 컴파일러가 임시 객체 내부의 참조를 저장한다고 판단하면
// 전체 임시 객체를 유지합니다.
const std::string& person_name = GeneratePerson().name; // 안전
// 그러나 컴파일러가 이를 판단하지 못하면 위험할 수 있습니다.
const std::string& nickname = GenerateDiceRoll().nickname(); // 위험!
옵션 4: 객체를 반환하지 않는 함수 설계?
많은 함수가 이 원칙을 따르지만, 모든 함수가 그런 것은 아닙니다. 경우에 따라 객체를 반환하는 것이 호출자가 출력 매개변수를 전달하도록 요구하는 것보다 더 나을 수 있습니다. 임시 객체가 생성될 수 있는 상황을 인지해야 합니다. 객체 내부의 포인터 또는 참조를 반환하는 모든 함수는 잠재적으로 임시 객체에서 동작할 때 문제가 될 수 있습니다. c_str()
은 가장 명백한 원인 중 하나이지만, 프로토버퍼의 getter(변경 가능한 것 포함)나 일반적인 getter도 동일한 문제가 발생할 수 있습니다.
요약
임시 객체의 수명 관리와 관련된 함정을 피하기 위해 항상 임시 객체가 언제 소멸하는지 주의하고, 가능한 안전한 방법으로 객체를 저장하거나 사용하세요.