KoreanFoodie's Study

Effective Modern C++ | 항목 24 : 보편 참조와 오른값 참조를 구별하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 24 : 보편 참조와 오른값 참조를 구별하라

GoldGiver 2022. 10. 26. 09:58

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 24 : 보편 참조와 오른값 참조를 구별하라

핵심 :

1. 함수 템플릿 매개변수의 형식이 T&& 형태이고 T 가 연역된다면, 또는 객체를 auto&& 로 선언한다면, 그 매개변수나 객체는 보편 참조이다.
2. 형식 선언의 형태가 정확히 형식&& 가 아니면, 또는 형식 연역이 일어나지 않으면, 형식&& 는 오른값 참조를 뜻한다.
3. 오른값으로 초기화되는 보편 참조는 오른값 참조에 해당한다. 왼값으로 초기화되는 보편 참조는 왼값 참조에 해당한다.

 

"T&&" 는 오른값 참조 또는 왼값 참조 중 하나라는 뜻이다. 이러한 참조를 보편 참조(universal reference) 라고 부른다. 예시를 보자.

// 형식 연역이 일어나는 예시 1
template<typename T>
void f(T&& param);

// 형식 연역이 일어나는 예시 2
auto&& var2 = var1;

// 형식 연역 없음 - 오른값 참조!
void f(Widget&& param);
Widget&& var1 = Widget();

...

Widget w;
// f 에 왼값 전달
// param 의 형식은 Widget& (왼값 참조)
f(w);

// f 에 오른값 전달
// param 의 형식은 Widget&& (오른값 참조)
f(std::move(w));

하나의 참조가 보편 참조이려면 반드시 형식 연역이 관여해야 한다. 또한, 참조 선언의 형태(form) 도 정확해야 한다. 구체적으로 말해서, 딱 "T&&" 의 형태이어야 한다.

// param 은 오른값 참조 (보편 참조가 아님)
template<typename T>
void f(std::vector<T>&& param);

std::vector<int> v;
f(v); // 오류! 왼값을 오른값에 묶을 수 없음

 

그리고 "T&&" 라고 다 보편 참조는 아니다. 템플릿 안에서 형식 연역이 반드시 일어난다는 보장이 없기 때문이다. 다음의 예시를 보자.

// std::vector 의 push_back 멤버 함수 발췌
template<class T, class Allocator = allocator<T>>
class vector {
	void push_back(T&& x);
	...
};

push_back 의 매개 변수는 보편 참조가 요구하는 형태이지만, 이 경우 형식 연역이 일어나지 않는다. push_back 은 반드시 구체적으로 인스턴스화된 vector 의 일부이어야 하며, 그 인스턴스의 형식은 push_back 의 선언을 완전하게 결정하기 때문이다. 위의 std::vector 템플릿은 다음과 같이 인스턴스화되어, push_back 에는 어떤 형식 연역도 관여하지 않게 된다.

class vector<Widget, allocator<Widget>> {
	void push_back(Widget&& x);
	...
};

 

반면, push_back 과 비슷한 emplace_back 함수는 실제로 형식 연역을 진행한다.

// std::vector 의 emplace_back 멤버 함수 발췌
template<class T, class Allocator = allocator<T>>
class vector {

	template<class... Args>
	void emplace_back(Args&&... args);
	...
};

args 는 T 와 독립적이며, 호출될 때마다 형식이 연역될 것이다.

 

auto 변수 역시 보편 참조가 될 수 있다. 정확히 말하면, auto&& 를 형식으로 해서 선언된 변수는 보편 참조이다. 형태("T&&") 가 정확하기 때문이다. 예를 들어, 임의의 함수 호출 시간에 걸린 시간을 기록하는 C++14 버전 람다는 다음과 같이 구현할 수 있다.

auto timeFuncInvocation =
	[](auto&& func, auto&&... params)
	{
		// 타이머 시작;
		std::forward<decltype(func)>(func)(
			std::forward<decltype(params)>(params)...
			);
		// 타이머 정지 후 경과 시간 기록;
	};

decltype ... 관련된 부분은 항목 33에서 자세히 다룬다. 핵심은, func 가 그 어떤 호출 가능 객체(왼값이든 오른값이든)와도 묶일 수 있는 보편 참조라는 것이다. params 도 마찬가지이다. auto 보편 참조 덕분에, 위 함수는 거의 모든 함수의 실행 시간을 측정할 수 있다("모든" 이 아니라 "거의 모든"인 이유는 항목 30에서 밝혀진다).

마지막으로, 보편 참조의 기본을 설명하는 이 항목 자체가 '추상'임을 명시하자. 바탕에 깔린 진실은 참조 축약(reference collapsing) 이라는 것인데, 이에 관해서는 항목 28에서 다루기로 하겠다!

Comments