주간 팁 #101: 반환 값, 참조 및 수명
Originally posted as totw/101 on 2015-07-29
By Titus Winters (titus@google.com)
다음의 코드 스니펫을 보겠습니다:
const string& name = obj.GetName();
std::unique_ptr<Consumer> consumer(new Consumer(name));
특히 여기서 &
에 주목해주세요. 이것이 적절할까요? 우리는 무엇을 확인해야 할까요? 어떤 문제가 발생할 수 있을까요? 많은 C++ 프로그래머들이 참조에 대해 완전히 이해하지 못한 상태에서 “복사를 피한다”고만 알고 있는 경우가 많습니다. 하지만 C++의 대부분의 문제처럼, 이 문제도 더 복잡합니다.
사례별 분석: 반환되는 값과 저장되는 방식
여기서 중요한 질문은 두세 가지가 있습니다:
- 반환되는 타입은 무엇인가요? (이 예제에서는
GetName()
에 의해 반환되는 타입) - 우리가 저장하거나 초기화하는 타입은 무엇인가요? (이 예제에서는
name
의 타입) - 만약 참조를 반환하는 경우, 반환된 참조가 가지는 수명에 제한이 있는가요?
여기서는 string
타입을 예로 들지만, 이 논의는 대부분의 비트리비얼(non-trivial) 값 타입에 일반화될 수 있습니다.
string
반환,string
초기화: 이는 보통 RVO(Return Value Optimization)이 적용되며, 현대적인 타입에서는 최악의 경우 이동(move)만 발생합니다 (TotW 77 참고).string&
또는const string&
반환,string
초기화: 이 경우 복사가 발생합니다 (참조를 반환하는 경우, 해당 데이터를 가리키는 이름이 두 개 생기므로 복사가 필요합니다. TotW 77 참고). 하지만 함수가 제공하는 수명 보장보다 더 오래string
이 필요할 경우 유용할 수 있습니다.string
반환,string&
초기화: 이는 컴파일되지 않습니다. 임시 객체에 참조를 바인딩할 수 없기 때문입니다.const string&
반환,string&
초기화: 이것도 컴파일되지 않습니다.const
를 부적절하게 제거했기 때문입니다.const string&
반환,const string&
초기화: 비용이 들지 않습니다 (사실상 포인터를 반환하는 것과 유사). 하지만 기존 수명 제한을 상속받게 됩니다. 대부분의 접근자 메서드가 참조를 반환할 때 멤버를 반환하므로, 반환된 참조는 포함된 객체의 수명 동안에만 유효합니다.string&
반환,string&
초기화: #5와 동일하지만, 추가적인 경고 사항이 있습니다. 반환된 참조가const
가 아니므로, 참조를 통해 변경하면 원본에 반영됩니다.string&
반환,const string&
초기화: #5와 동일합니다.string
반환,const string&
초기화: #3을 생각해보면 이게 작동하지 않을 것 같지만, 언어 차원에서 예외 처리가 있습니다.const T&
를 임시T
로 초기화하면 해당T
(여기서는string
)는 참조가 스코프를 벗어날 때까지 파괴되지 않습니다 (일반적인 경우 자동 변수나 정적 변수).
시나리오 #8은 대부분의 참조 사용을 반사적으로 허용합니다 (즉, “복사를 피하고 싶으니 참조에 할당하겠다”라고 별다른 생각 없이 사용하는 경우). 하지만 #1에서 본 것처럼, 실제로는 큰 이점이 없습니다. 애초에 복사가 발생하지 않았을 가능성이 높기 때문입니다. 게다가, 코드 독자는 const string&
타입의 지역 변수가 실제로 스코프를 벗어나거나 변경되었는지 여부를 걱정해야 합니다.
다시 말해, 원래의 코드 스니펫을 코드 리뷰할 때 다음 사항을 걱정해야 합니다:
GetName()
이 값으로 반환하는가, 참조로 반환하는가?Consumer
의 생성자는string
,const string&
또는string_view
를 받는가?- 생성자가 해당 매개변수에 대해 수명 제한을 요구하는가? (만약
string
이 아닌 경우)
반면, 처음부터 name
을 string
으로 선언하면 RVO와 이동 의미론 덕분에 성능 저하가 없으며, 객체 수명 측면에서도 안전할 가능성이 높습니다.
추가로, 객체 수명 문제가 있는 경우, 참조의 수명 보장과 SetName()
의 수명 요구 사항 간의 상호 작용을 살피기보다, 자신의 string
을 사용하는 것이 로컬 코드만 살피면 되므로 문제를 더 쉽게 찾을 수 있습니다.
즉, 복사를 피하는 것은 좋지만, 불필요한 복잡성을 추가하지 않는 것이 중요합니다. 애초에 복사가 없었을 경우 복잡성을 추가하는 것은 좋은 선택이 아닙니다.