Modern C++/Move Semantics

3. 완벽 전달(Perfect Forwarding)

basker 2020. 6. 29. 23:45

완벽 전달은 임의의 인수를 받아서 이를 다른 함수로 전달할 때 대상 함수에 전달 함수가 받은 것과 동일한 인수를 받아들 일 수 있게 한다. 완벽 전달을 이해하기 위해서 보편참조에 대해 살펴보자. 

 

오른쪽 참조와 보편 참조의 구분

타입 T 대한 오른값 참조는 T&& 표기하고 이는 오직 오른값에 바인딩되어 해당 객체가 이동이   있다는  표현한다. 

void f(int&& a); // int&& 오른값 참조

int&& a = 10; //int&& 오른값 참조

T&&  형식 추론(Type Deduction) 엮이면 해당 표기는 보편 참조를 나타낸다. 이는 오른값 참조와 왼값 참조 모두에 바인드   있다. 

보편 참조는 형식 추론이 일어나는 템플릿 함수의 매개변수나 auto 선언에서   있다. 

auto&& b = a; //auto&&  보편 참조

template <typename T>

void f(T&& param); // T&& 보편참조

따라서 형식 추론이 발생하지 않는 곳에서의 타입 T&& 오른값 참조이고 반대이면 보편참조가 된다. 

template <typename T>

std::queue<T> convert_queue(std::vector<T>&& param) //오른값 참조

위의 템플릿 함수는 파라미터 std::vector<T>&&  대한 형식 추론이 발생하지 않으므로 오른값 참조이다. 

 

 

#include <iostream>
#include <boost/type_index.hpp>

using namespace std;
using namespace boost;
 
template <typename T>
void translate_type(T&& arg) {
    using boost::typeindex::type_id_with_cvr;
    std::cout << "T= " << type_id_with_cvr<T>().pretty_name() << std::endl;
    std::cout << "ParamType = " << type_id_with_cvr<T&&>().pretty_name() << std::endl;
}
 
int main() {
	int x = 3;
	const int cx = x;
	const int& rx = x;
	translate_type(x); // x는 왼값 T = int& , ParmType = int&
	translate_type(cx); // cx는 왼값 T = const int&, ParamType = const int&
	translate_type(rx); // rx는 왼값 T = const int&, ParamType = const int&
	translate_type(5); //5는 오른 값 T = int, ParamType = int&&
	translate_type(std::move(x)); //std::move(x) 오른 값 참조 T = int, ParamType = int&&
	return 0;
}

 

보편 참조는 참조에 해당되므로 반드시 초기화 되어야 하고, 초기화  오른값 참조인지 왼값 참조인지 결정된다. 

보편 참조가 초기화   왼값으로 초기화 되면 왼값 참조, 오른값이면 오른값 참조가 된다. 

 

다시 한번 강조하자면 타입 T&&  형식 추론이 발생한다면 해당 참조는 보편 참조가 되고, 보편 참조는 왼값으로 초기화되면 왼값 참조, 오른쪽 값으로 초기화되면 오른쪽 참조가 된다.

 

std::forward

std::forward는 전달 함수에 들어온 인수의 타입을 대상함수에 정확하게 전달하기 위해서 사용된다. 

std::forward
template <class T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept;
template <class T>
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept;
Return Value
static_cast<T&&>(t)
void func_destination(int && a) {
    std::cout << "parameter is rvalue reference" << std::end;
}

void func_destination(const int& a) {
    std::cout << "parameter is lvalue reference" << std::endl;
}

template <typename T>
void func_source(T&& param) {
    func_destination(param);
}

int x = 10;
func_source(x); //  func_source(int&)
func_source(10); // func_source(int&&)

출력 
parameter is lvalue reference
parameter is lvalue reference

func_source() 함수가 매개 변수가 보편참조로 해당 매개변수로 들어온 형식에 따라 재정의된 두 함수 func_destination 를호출하기 위해 위와 같이 코드를 작성한다고 가정하자. func_source(x)는 왼값 참조이므로 fun_destination(const int& a) 가 호출 되고, func_source(10)은 오른값 이므로 func_destination(int&& a)가 호출 될거라 생각하지만, 두 함수 호출은 모두 func_destination(const int& a)를 호출한다.

이유는 간단하게 func_source 의 param 자체가 왼값이므로 두 함수 모두 왼값 참조에 해당하는  함수를 호출하게 된다.

이처럼 매개 변수를 전달할 함수가 매개 변수의 타입을 완벽하게 전달하기 위해서 std::forward()가 사용된다.

template <typename T>
void func_source(T&& param) {
    func_destination(std::forward<T>(param));
}

std::forward 파라미터가 왼값인 경우 왼값 참조를 오른값인 경우 오른값 참조로 static_cast 된다. 

std::forward 가 실제 완벽 전달을 수행하지 않고 static_cast 된다는 점은 std::move와 닮았다. 차이점은 std::move는 왼값, 오른값에 관계없이 오른쪽 참조로 static_cast 되지만 std::forward는 파라미터 타입에 따라 왼값 참조나 오른값 참조로 static_cast 된다는 점이다.