[ Modern C++ : std::Forward ]
전달 참조 (Forwarding Reference)
이전에는 보편 참조(Universal Reference)로 불리었으나, C++17부터 전달 참조(Forwarding Reference)라는 명칭으로 바뀌었다.
class Knight
{
public:
Knight() { cout << "기본 생성자" << endl; }
Knight(const Knight&) { cout << "복사 생성자" << endl; }
Knight(Knight&&) noexcept { cout << "이동 생성자" << endl; }
}
void Test_RValueRef(Knight&& k) // 오른값 참조
{
}
template<typename T>
void Test_ForwardingRef(T&& param)
{
}
int main()
{
Knight k1;
Test_RValueRef(std::move(k1)); // std::move()를 통해 오른값으로 캐스팅하지 않으면 통과가 되지 않음.
// 1) template에 의한 type deduction
Test_ForwardingRef(k1); // l-value reference로 type deduction (knight&)
Test_ForwardingRef(std::move(k1)); // r-value reference로 type deduction (knight&&)
// 2) auto에 의한 type deduction
auto&& k2 = k1; // l-value reference로 type deduction (knight&)
auto&& k2 = std::move(k1); // r-value reference로 type deduction (knight&&)
}
의문 : &&가 등장하면 무조건 오른값 참조일까?
template이나 auto처럼 형식 연역 (type deduction)이 발생할 때 자주 일어남.
1) template에 의한 type deduction
위 코드의 Test_ForwardingRef()에서, 원래 l-value인 k1을 넘겨줄 경우 r-value만 받아주는 함수이므로 통과가 되지 않지만, template에 의해 type deduction이 발생할 경우 k1을 l-value reference로 바꿔줘서 통과가 일어나는걸 알 수 있음.
2) auto에 의한 type deduction
위 코드의 auto&& k2 = k1; 에서 l-value인 k1이 auto에 의해 knight& 로 바뀌어서 통과가 됨.
일반적인 상황에서 이런 일이 일어나진 않고, template, auto 같은 문법에서 발생.
공통점 : type deduction (형식 연역, 형식 추론)
만약 const 같은 부가적인걸 붙여준다면 더 이상 통과되지 않음. (순수한 T&& 또는 auto&& 일 때만 발생)
[ 왜 이런걸 만들었을까? ]
왼값, 오른값을 받는 버전을 따로따로 만들어야 한다면 2개의 함수를 만들어줘야 하는데, typname이 T1, T2로 2개가 된다면 경우의 수마다 왼값, 오른값을 버전을 따로 만들어 줘야 하므로 총 4개의 함수를 만들어줘야하는 일이 발생한다.
→ template이나 auto을 사용할 때, 공통되게 사용할 수 있게 전달 참조(Forwarding Reference)라는 문법을 만들어줬다고 예상할 수 있다.
프로그래머가 인자로 넣어주는 값에 따라 l-value reference, r-value reference 두 가지로 나뉘어 동작할 수 있기 때문에 케이스를 나눠서 함수를 만들어줄 필요가 없어진다.
/* --------------------------------------------------------------- */
template<typename T>
void Test_ForwardingRef(T&& param) // 전달 참조
{
// TODO : param의 원본은 왼값? 오른값?
std::move(param);
// param을 가지고 다른 일을 하고 싶어요
Test_Copy(param);
}
그래서 위의 경우 오른값 참조를 인자로 받는 함수를 하나로 사용하는 장점이 있는데, 인자로 왼값이 들어올 수도 있고 오른 값이 들어올 수도 있다는 말이 된다.
인자가 오른값이었을 경우 함수내에서 다시 std::move()를 사용해서 어떤 일을 하고 싶을 수가 있는데, 인자의 원본이 "왼값" 이었을 경우 std::move()를 사용한다면 원본이 훼손되므로 난감한 상황이 되는 것이다.
→ 인자는 "오른값 참조" 타입이지만 원본이 왼값, 오른값 중 어떤 것이냐에 따라 동작하는 어떤 기능이 필요해진 것이다.
void Test_RValueRef(Knight&& k) // 오른값 참조
{
}
Knight k1;
Knight& k4 = k1; // 왼값 참조
Knight&& k5 = std::move(k1); // 오른값 참조
// 오른값의 정의 : 왼값이 아니다 -> "단일 식"에서 벗어나면 사용 못함
// 오른값 참조 : 오른값만 참조할 수 있는 참조 타입
Test_RValueRef(k5); // Error : 오른값 참조 타입이지만 오른값은 아니다.
Test_RValueRef(std::move(k5));
→ Knight&& k5 = std::move(k1);를 통해 k1을 오른값으로 넘겨줘서 "오른값 참조인" k5에 저장했다 하더라도 오른값의 엄밀한 정의는 ["단일 식"에서 벗어나면 사용하지 못하는 것]이므로 k5는 오른값 참조타입이지만 원본은 왼값에 해당한다.
그러므로 Test_RValueRef(k5)는 에러를 뱉는 것이고, 다시 std::move()를 통해 오른값으로 캐스팅 해줘야 통과가 되는 것이다.
이 예시에서 가장 중요한 것은, "오른값 참조타입이라고 해도, 항상 오른값이 아니다"
class Knight
{
public:
Knight() { cout << "기본 생성자" << endl; }
Knight(const Knight&) { cout << "복사 생성자" << endl; }
Knight(Knight&&) noexcept { cout << "이동 생성자" << endl; }
}
void Test_Copy(Knight k)
{
}
template<typename T>
void Test_ForwardingRef(T&& param)
{
// TODO : type deduction이 발생함에 따라 lvalue ref, rvalue ref 둘 다
// 이 함수에 들어올 수 있으므로, 두 가지 경우에 따라 얕은 복사, 깊은 복사를 하도록
// 해야한다.
Test_Copy(param); // 원본이 왼값 참조라면, 복사 생성자를 호출해야 함.
Test_Copy(std::move(param)); // 원본이 오른값 참조라면, 이동 생성자를 호출해야 함.
}
int main()
{
knight k1;
Test_ForwardingRef(std::move(k1));
}
이제 위 예시에서 Test_ForwardingRef(std::move(k1));을 실행하면, 원본이 왼값인 k1을 오른값으로 바꿔서 넣어준 것이므로, Test_ForwardingRef(T&& param) 내에서 params은 오른값 참조 타입이지만, 원본은 "왼값"이므로 Test_Copy(param)이 실행되면 Knight의 이동 생성자가 아닌 "복사 생성자"가 실행될 것이다.
이동 생성자를 실행시켜주게 하기 위해선 다시 std::move()를 통해 오른값으로 캐스팅해줘서 넘겨줘야하는 것이다.
그런데, 위 예시에서 원본이 왼값인 k1을 std::move()를 통해 멋대로 이동 생성자를 호출하는 것은 사실 말이 안되는 문제가 발생할 것이므로 원본이 "왼값" 또는 "오른값" 이냐에 따라 분기해야하는 것이다.
1) 원본이 왼값 참조일 경우 : 복사
TestCopy(param);
2) 원본이 오른값 참조일 경우 : 이동
TestCopy(std::move(param));
[ std::forward ]
template<typename T>
void Test_ForwardingRef(T&& param)
{
Test_Copy(std::forward<T>(param));
}
→ param의 원본이 왼값 참조라면 복사 생성자 호출, 오른값 참조라면 이동 생성자 호출
[ Reference ]
Rookiss, C++ 입문 Part1. 전달 참조(Forwarding Reference)
'컴퓨터공학 > C++' 카테고리의 다른 글
[More Effective C++] 24 : 가상함수, 다중상속, 가상 기본 클래스, RTTI에 들어가는 비용을 제대로 파악하자 (0) | 2024.11.03 |
---|---|
[Modern C++] Smart Pointer (1) | 2024.09.27 |
[Modern C++] Lambda (0) | 2024.09.11 |
[C++] 동적할당 (0) | 2024.08.23 |
[C++] 초기화 리스트 (0) | 2024.08.22 |