[Modern C++] 항목 24: 보편 참조와 rvalue 참조를 구별하라
카테고리: Cpp
이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역
가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩
📦 5. rvalue 참조, 이동 개념, 완벽 전달
👉🏻 항목 24: 보편 참조와 rvalue 참조를 구별하라
🔍 “T&&”의 두 가지 의미
- 소스 코드에서
"T&&"형태로 보인다고 해서 항상 rvalue 참조인 것은 아니다."T&&"에는 서로 다른 두 가지 의미가 있다.- rvalue 참조: 예상한 그대로 행동한다. 오직 rvalue에만 묶이며, 존재 이유는 이동의 원본이 될 수 있는 객체를 지정하는 것이다.
- 보편 참조(universal reference): 소스 코드에서는 rvalue 참조(
"T&&")처럼 보이지만, 때로는 lvalue 참조("T&")인 것처럼 행동한다. rvalue에도, lvalue에도 묶일 수 있고, const/비const, volatile/비volatile 객체 모두에 묶일 수 있다. 즉, 거의 모든 것에 묶일 수 있다.- 항목 25에서 설명하듯이 보편 참조에는 거의 항상
std::forward를 적용해야 하므로, C++ 공동체에서는 이를 전달 참조(forwarding reference)라고도 부른다.
- 항목 25에서 설명하듯이 보편 참조에는 거의 항상
template<typename T>
void f(T&& param); // param은 보편 참조
🔍 보편 참조가 나타나는 두 가지 문맥
① 함수 템플릿 매개변수
template<typename T>
void f(T&& param); // param은 보편 참조
Widget w;
f(w); // f에 lvalue가 전달됨; param의 타입은
// Widget&(즉, lvalue 참조)
f(std::move(w)); // f에 rvalue가 전달됨; param의 타입은
// Widget&&(즉, rvalue 참조)
② auto 선언
auto&& var2 = var1; // var2는 보편 참조
- 두 문맥의 공통점은 타입 추론(type deduction)이 일어난다는 점이다.
f에서는param의 타입이 추론되고,var2선언에서는var2의 타입이 추론된다. - 타입 추론이 일어나지 않는 문맥에서
"T&&"를 발견했다면, 그것은 rvalue 참조다.
void f(Widget&& param); // 타입 추론 없음
// param은 rvalue 참조
Widget&& var1 = Widget(); // 타입 추론 없음
// var1은 rvalue 참조
🔍 보편 참조의 성질 판별 — 초기값이 결정
- 보편 참조는 참조이므로 반드시 초기화해야 한다. 초기값이 rvalue이면 보편 참조는 rvalue 참조에 해당하고, 초기값이 lvalue이면 lvalue 참조에 해당한다.
- 보편 참조가 함수의 매개변수인 경우, 초기값은 그 함수를 호출하는 지점에서 제공된다.
🔍 형태(form)가 정확해야 한다 — 딱 “T&&”이어야 함
- 타입 추론이 일어나는 것은 필요조건일 뿐 충분조건은 아니다. 참조 선언의 형태도 반드시 정확히
"T&&"이어야 한다.
template<typename T>
void f(std::vector<T>&& param); // param은 rvalue 참조
f호출 시 타입T는 추론되지만,param의 타입 선언 형태가"T&&"가 아니라"std::vector<T>&&"이므로param은 보편 참조가 될 수 없다. 따라서param은 rvalue 참조다. 이 점은f에 lvalue를 전달해 보면 컴파일러가 확인해 준다.
std::vector<int> v;
f(v); // 오류! lvalue를 rvalue
// 참조에 묶을 수 없음
const한정자 하나만 붙어도 참조는 보편 참조가 되지 못한다.
template<typename T>
void f(const T&& param); // param은 rvalue 참조
🔍 템플릿 안 “T&&”라고 다 보편 참조는 아니다 — 타입 추론이 일어나지 않는 경우
std::vector::push_back을 보자.
template<class T, class Allocator = allocator<T>> // C++ 표준에서 발췌
class vector {
public:
void push_back(T&& x);
…
};
push_back의 매개변수는 형태상 보편 참조가 요구하는 조건과 일치하지만, 이 경우에는 타입 추론이 전혀 일어나지 않는다.push_back은 반드시 구체적으로 인스턴스화된vector의 일부여야 하고, 그 인스턴스의 타입이 이미push_back의 선언을 완전히 결정하기 때문이다.
std::vector<Widget> v;
- 이 선언에 의해
vector템플릿은 다음과 같이 인스턴스화된다.
class vector<Widget, allocator<Widget>> {
public:
void push_back(Widget&& x); // rvalue 참조
…
};
- 반면, 개념적으로 비슷한
emplace_back멤버 함수는 실제로 타입 추론을 사용한다.
template<class T, class Allocator = allocator<T>> // 역시 C++ 표준에서 발췌
class vector {
public:
template<class... Args>
void emplace_back(Args&&... args);
…
};
Args는vector의 타입 매개변수T와 독립적이며,emplace_back이 호출될 때마다 추론되어야 한다. (엄밀히는Args는 타입 매개변수가 아니라 매개변수 팩(parameter pack)이지만, 논의 목적상 타입 매개변수로 간주해도 무방하다.)emplace_back의 타입 매개변수 이름이T가 아니라Args이지만 여전히 보편 참조라는 점은, 보편 참조의 형태를 반드시T&&라고 할 필요가 없음을 보여준다.
template<typename MyTemplateType>
void someFunc(MyTemplateType&& param); // param은 보편 참조
🔍 auto&& — 람다에서의 보편 참조
auto&&를 타입으로 해서 선언된 변수 역시 타입 추론이 일어나고 형태("T&&")가 정확하므로 보편 참조다.- C++14에서는 람다 표현식에서
auto&&매개변수를 선언할 수 있어 더 자주 나타난다.
auto timeFuncInvocation =
[](auto&& func, auto&&... params) // C++14
{
타이머를 시작한다;
std::forward<decltype(func)>(func)( // params로
std::forward<decltype(params)>(params)... // func를 호출
);
타이머를 정지하고 경과 시간을 기록한다;
};
func는 어떤 호출 가능 객체(lvalue든 rvalue든)와도 묶일 수 있는 보편 참조이고,params는 임의의 타입, 임의의 개수의 객체들과 묶일 수 있는 0개 이상의 보편 참조로 이루어진 매개변수 팩이다.auto보편 참조 덕분에timeFuncInvocation은 거의 모든 함수의 실행 시간을 측정할 수 있다(“모든”이 아니라 “거의 모든”인 이유는 항목 30에서 밝혀진다).
🔍 이 항목은 하나의 ‘추상’이다
- 이 항목 전체가 사실은 하나의 추상(abstraction)이며, 그 바탕에 깔린 진실은 참조 축약(reference collapsing)이라는 것이다(항목 28에서 중점적으로 다룸).
- 그럼에도 이 추상은 유용하다. rvalue 참조와 보편 참조를 구분하면 소스 코드를 더 정확하게 읽는 데 도움이 되고(“이 T&&가 rvalue에만 묶일까, 아무것에나 묶일까?”), 동료와의 대화에서 중의성을 피할 수 있다(“이 부분은 rvalue 참조가 아니라 보편 참조인데요…”). 또한 항목 25, 26을 이해하는 데에도 필요하다.
🧐 정리
- 함수 템플릿 매개변수의 타입이
T&&형태이고T가 추론된다면, 또는 객체를auto&&로 선언한다면, 그 매개변수나 객체는 보편 참조다. - 타입 선언의 형태가 정확히
타입&&가 아니거나, 타입 추론이 일어나지 않으면,타입&&는 rvalue 참조를 뜻한다. - rvalue로 초기화되는 보편 참조는 rvalue 참조에 해당한다. lvalue로 초기화되는 보편 참조는 lvalue 참조에 해당한다.
댓글남기기