dev/C++

Effective Modern C++ #24 보편 참조와 오른값 참조(r-value ref)를 구별하라.

dev_dev 2025. 6. 22. 13:22

24. 보편 참조와 오른값 참조(r-value ref)를 구별하라.

보편참조는 추상적이다. ( 선언 시 l-value, r-value 로 나뉘지 않음 )

 

일반적으로 (템플릿)T 형식에 대한 r-value reference 선언 시, T&& 표기를 사용한다.

하지만 반대로 코드에서 T&& 를 발견했을 때 이를 r-value reference 라고 단언할 수 없다.

 

다양한 예시)

void f(Widget && param); // r-value ref

Widget&& var1 = Widget(); // r-value ref

auto&& var2 = var1; // r-value ref 아님

template<typename T>
void f(std::vector<T>&& param); // r-value ref

template<typename T>
void f(T&& param); // r-value ref 아님

 

"T&&" 형식이 될 수 있는 것

  1. r-value reference
    • r-value 인자를 사용하며, 이동의 원본이 되는 객체 지정함
  2. universal reference
    • 추상적 ( l-value 또는 r-value reference )
    • l-value, r-value 와 const, 비 const, volatile 등 거의 모든 인자에 대응할 수 있음

 

universal reference 는 두가지 문맥에서 나타난다.

1. 함수 템플릿 매개변수

template<typename T>
void f(T&& param);

2. auto&& 선언

auto&& var2 = val1;

 

위 두 케이스의 공통점은 형식 연역이 일어난다는 것이다.

보편 참조를 만드는 조건은 다음과 같다.

  1. 형식 연역이 관여해야함
  2. 참조 선언의 형태 정확해야함 (T&& 의 형태)

 

함수 템플릿 매개변수

형식 연역 관여되지 않는 예시1)

template<typename T>
void f(T&& param) {}

f<int>(x);	// r-value ref

 

위 케이스와 같이 호출자에서 T 가 명시적으로 지정되는 경우, 형식 연역이 관여하지 않는다.

따라서 보편 참조가 아닌 r-value reference 로 동작한다.

 

형식 연역 관여되지 않는 예시2)

#include<iostream>
#include<vector>

template<typename T>
void f(std::vector<T>&& param) {
    std::cout << "r-value reference" << std::endl;
}

int main() {
    std::vector<int> v{10, 20};
    f(v);	// r-value ref 이나, l-value 인자이므로 컴파일 에러
}

위 케이스는 T&& 형태가 아닌 std::vector<T>&& 형태이다.
따라서 f 는 r-value reference 로 동작하고, l-value 넣었을 때 컴파일 에러 발생한다.

 

보편 참조가 되기 위한 참조 선언의 형태는 상당히 엄격하다.

예를 들어 T&& 에 const 만 붙여도 보편 참조가 되지 못한다.

 

또한 템플릿 안에서 T&& 형태 함수 매개변수를 사용한다고 보편 참조라고 확신할 수 없다.

형식 연역이 반드시 일어나는 보장이 없기 때문이다.

 

std::vector push_back 예시)

template<class T, class Allocator = allocator<T>>
class vector {
	public:
		void push_back(T&& x);
		...
};

std::vector<Widget> v;

위 케이스에서 클래스 생성 시 T 가 Widget 으로 연역된다.

따라서 push_back 멤버 함수에는 형식 연역이 관여하지 않는다.

이러한 경우 멤버 함수는 r-value reference 로 동작한다.

 

반면 emplace_back 은 멤버 함수에서 형식 연역이 사용된다.

std::vector emplace_back 예시)

template<class T, class Allocator = allocator<T>>
class vector {
	public:
		template<class... Args>
		void emplace_back(Args&&... x);
		...
};

여기서 Args 는 vector 형식 매개변수 T 와 독립적이다.

Args 는 emplace_back 호출될 때마다 그 형태가 연역된다.

 

auto&& 선언

auto&& 형식으로 선언된 경우, 형식 연역이 일어나고 형태(T&&)가 정확하므로 보편참조이다.

C++14 이상에서는 람다표현식에서 auto&& 매개변수 선언을 사용한다.

auto&& 매개변수 사용 예시)

auto timeFuncInvoation = [] (auto&& func, auto&&... params) {
	std::forward<decltype(func)>(func)(
		std::forward<decltype(params)>(params)...
	);
}

func, params 모두 보편 참조이므로, 거의 모든 함수의 실행시간을 확인할 수 있는 함수가 된다.

 

보편 참조는 기본적으로 추상적이나 유용하며, r-value ref 와 universal ref 를 구분하는 것은 도움이 된다.