&& 얘는 이전 시간에 오른값참조를 받기 위해서만 쓰이는 것처럼 보였다.
그런데 꼭 그렇지만은 않으므로 알아보자.
우선 지난시간의 오른값 참조와 이동연산자인 std::move의 복습이다.
#include <iostream>
using namespace std;
// 오른값 참조로 인자를 받으면 전달한 객체를 없애도 된다는 힌트를 준다.
// 값 복사가 일어나야하는 상황에서도 복사대신 이동을 시켜 깊은복사 대신 이동으로
// 최적화의 여지가 생긴다
class Knight
{
public:
Knight() {cout<<"기본 생성자"<<endl;}
Knight(const Knight& k) {cout<<"복사 생성자"<<endl;}
Knight(Knight&& k) {cout<<"이동 생성자"<<endl;}
}
void Test_RValueRef(Knight&& k)
{
}
int main()
{
Knight k1;
Test_RValueRef(std::move(k1));
// 오른값만 받으므로 move로 왼값인 k1을 오른값으로 바꿔치기 해야한다.
// move는 오른값 참조로 캐스팅 역할이 전부다.
return 0;
}
특히 전달참조는 템플릿이나 auto같이 형식 연역이라고 해서 타입 추론(deduce, type guessing)이 일어 날 때
#include <algorithm>
class Knight
{
public:
int _hp = 10;
public:
Knight() {cout<<"기본 생성자"<<endl;}
Knight(const Knight& k) {cout<<"복사 생성자"<<endl;}
Knight(Knight&& k) {cout<<"이동 생성자"<<endl;}
}
void Test_RValueRef(Knight&& k)
{
}
template<typename T>
void Test_ForwardingRef(T&& param)
{
}
int main()
{
Knight k1;
Test_RValueRef(std::move(k1));
// 오른값만 받으므로 move로 왼값인 k1을 오른값으로 바꿔치기 해야한다.
// move는 오른값 참조로 캐스팅 역할이 전부다.
// 타입 추론이 일어나서 Knight의 오른값을 전달하나 싶다.
Test_ForwardingRef(std::move(k1)); // void Test_ForwardingRef<Knight>(Knight &¶m)
// 그냥 왼값만 받아도 통과 된다.?
// 심지어 왼값참조.?
Test_ForwardingRef(k1); // void Test_ForwardingRef<Knight &>(Knight ¶m)
//auto를 사용할때 *나 const를 쓸 수 있었던 것처럼 &&도 붙힐 수 있다.
auto&& k2 = k1;
// k1을 오른값으로 바꿔서 넘겨주었다.
auto&& k3 = std::move(k1);
return 0;
}
k2는 타입이 오른값 참조가 아닐까 싶은데 왼값이 k1을 그대로 받는거 보면 오른값 참조가 아니라는걸 알 수 있다.
마우스오버 해보면 auto&& k2는 Knight &k2로 왼값참조로 바뀌어 있는걸로 볼 수 있다.
그런데 바로 아래에서 k1을 std::move로 오른값으로 바꿔서 k3에 마우스오버해보면 Knight &&k3로 되어있다.
종합해보면 &&는 auto나 템플릿이랑 같이 쓰일 때 오른값참조타입으로도 쓰이고 왼값참조로도 쓰이는 전달참조가 생긴다. 이런 공통적 현상을 '형식 연역(type deduction)'이라고 하며 템플릿에서도 일어나는 것으로 아직은 정확히 정해지진 않았지만 넣어주는 값에 따라서 바뀌는 조커같은 역할이라 볼 수 있다.
전달참조의 특징을 보면
- 왼값을 넣어주면 왼값참조로 작동이 되고 오른값을 넣어주면 오른값 참조로 동작한다.
- TestForwardingRef의 매개변수 T&& param에 const만 붙여도 왼값을 받아주지 않게된다. const T&& param ( auto도 마찬가지)
✨&&가 등장한다고 해서 무조건 오른값참조를 받는게 아니며 auto나 템플릿이 같이 쓰일때 때에따라 왼값도 받는 전달참조가 된다.
그런데 왜 이런 전달참조를 만들게 되었을까?🤔
template<typename T>
void Test_ForwardingRef(T&& param) // 전달참조
void Test_ForwardingRef(const T&& param) // 오른값 참조
void Test_ForwardingRef(const T& param) // 왼값 참조
오른값을 받는 타입외에도 왼값을 참조로 받는 버전도 또 오버로딩으로 만들어서 쓰면 될일을.!
그런데 템플릿을 쓸 때는 꼭 하나씩만 쓰라는 법은 없다.
template<typename T, typename T2>
void Test_ForwardingRef(T&& param, T2&& param2)
그러면 T를 왼값으로 넘길때/오른값으로 넘길때 * T2를 왼값으로 넘길때/오른값으로 넘길때 4개의 함수를 오버로딩해야한다. 그리고 이런 경우는 타입을 여러개 받고싶을 때 그 수가 더 커지게 된다..
그래서 템플릿이나 오토를 쓸 때는 &&가 왼값도 참조로 받을 수 있고 오른값도 참조로 받을 수 있게 만든것이다.
그리고 전달참조로 받는다고 해도 왼값으로 받았냐, 오른값으로 받았냐에 따라 그 사용법이 달라져야한다.
// ...
void Test_Copy(Knight k)
{
}
template<typename T>
void Test_ForwardingRef(T&& param) // 전달참조
{
// ...
Test_Copy(param); // param이 왼값이었을 때 통과
}
위에서 Test_ForwardingRef의 param이 왼값(Test_ForwardingRef<Knight&>(Knight& param))이라면 Test_Copy에 바로 전달 할 시 매개변수가 Knight k 이므로 전달받은 인자는 복사되어 k 가 Knight의 복사생성자 Knight(const Knight&)가 호출되며 만들어진다.
반면에 Test_ForwardingRef의 param이 오른값이었다면(Test_ForwardingRef<Knight>(Knight &¶m)) 바로 Test_Copy에 전달 못하고 std::move(param)을 이용해 이동론을 이용하는 방법으로 전달해야한다. 그럼 이때 Test_Copy의 k는 이동생성자 Knight(Knight&&) 가 호출된다. 그리고 일반적으로 복사를 하는것보다 이동을 하는게 최적화의 여지가 많으며 빠르게 동작할 확률이 높다. - 복사생성자는 깊은 복사 방식으로 만드는게 일반적인 반면 이동생성자는 어차피 전달받은 대상이 더이상 쓰일 일이 없다는게 명확하니 이동방식으로 만들 수 있으니깐.
그래서 위 Test_ForwardingRef에서 보면 param이 왼값일 수도 있고 오른값일 수도 있어서 잘 못 사용하면 원본이 유지되어야하는 const Knight& 타입에 들어가야 할 값이 std::move로 소유권을 이전시키면 원본이 훼손될 수 있게된다.
그래서 왼값이냐 오른값이냐에 따라가지고 같이 동작을 하는 기능이 필요하다.
또 전달참조를 구별하는 방법을 알아보자.
// ...
Knight k1;
Knight& k4 = k1; // k4가 k1을 참조로 받았다.(왼값참조)
Knight&& k5 = std::move(k1); // 오른값 참조로 받아주는 k5
k4는 왼값참고이고 k5는 오른값참조이다 위에서 오른값참조를 인자로 받았던 Test_RValueRef에 k4를 넣어주면
// Test_RValueRef(k4); // error
당연히 오류가 난다.
그런데 k5도 그냥 넣어주면 오류가 난다.
// Test_RValueRef(k5); // error
오른값은 처음에 말했듯이 임시값인데 이를 소유권을 이동시키지 않고 감히 넘기려고 하다니..
k5는 오른값이라고 해서 오른값 참조에 넘길 수 없는것이다!!
오른값과 오른값 참조에 대하서 구분을 해보자.
오른값은 단일식에서 사용할 수 있는 왼값과 반대되어 단일식에서만 사용할 수 있었다.
그리고 오른값 참조는 오른값만 참조할 수 있는 참조타입이었다.
왼값을 오른값으로 쓰기 위해서 std::move를 사용하거나 static_cast<Knight&&>()를 사용해 오른값으로 캐스팅해서 써야했다.
그러나!! 위에서 k5가 오른값참조타입은 맞지만 k5가 오른값이냐는 또 별도의 문제이다.
// ...
cout<<k5._hp<<endl;
k5._hp = 100;
이렇게 오른값참조로 받은 k5를 이용해 계속해서 사용할 수도 있다. 즉, k5는 타입은 오른값참조가 맞기는 하지만 k5자체는 오른값은 아니라 왼값이라는 것이다.
그래서 Tes_RValueRef(std::move(k5)); 이렇게 이동시켜줘야한다.
즉, std::move로 뱉어주는 임시타입 자체는 재사용할 수 없고 Knight&& 타입은 이를 받아서 사용하는 것이다. 자세히 보면
Knight&& k5 = std::move(k1);
k5는 왼쪽에 있고 std::move는 왼쪽에 있지 않다..!(?)
결국 오른값 참조타입 자체가 원본 자체를 참조하고 있으면서 그 원본자체가 훼손되어도 상관없다는 의미이며, 오른값참조타입 자체는 오른값은 아니라는 것이다.
이렇게 auto 나 템플릿처럼 형식 연역이 일어나는 경우엔 원본의 타입이 무엇인지에 상관없이 바뀌기도 하는데 &&타입에 인자를 std::forward<T>()를 사용하면 const에 따라 오버로딩이 필요 없이 제대로 연역할 수 있게 한다. 이는 std::forward는 오른값으로 캐스팅하는 std::move와는 달리 인자로 받는 값이 왼값, 오른값, xvalue이냐에 따라 달라지기 때문이다.
https://en.cppreference.com/w/cpp/utility/forward
std::forward - cppreference.com
(1) (since C++11) (until C++14) (since C++14) (2) (since C++11) (until C++14) (since C++14) 1) Forwards lvalues as either lvalues or as rvalues, depending on T. When t is a forwarding reference (a function argument that is declared as an rvalue reference t
en.cppreference.com
다시 위로 돌아가보면
// ...
void Test_Copy(Knight k)
{
}
template<typename T>
void Test_ForwardingRef(T&& param) // 전달참조
{
// ...
Test_Copy(param); // param이 왼값이었을 때 통과
}
void Test_ForwardingRef(T&& param) 에서 T&& 가 전달참조로 param이 왼값참조로 받으면 당연히 param은 왼값이지만 전달참조가 오른값으로 받아도 param이 왼값이므로 Test_Copy(param)은 통과하게 된다..!
'[C++]' 카테고리의 다른 글
[C++ 20] Range (0) | 2024.12.09 |
---|---|
[C++ 20] Module (1) | 2024.11.13 |
Modern C++ #8 오른값 참조(rvalue reference)와 std::move (0) | 2024.10.22 |
shared_from_this, enable_shared_from_this (0) | 2024.09.06 |
STL#3 연관 컨테이너 - map, unordered_map, set, multimap, multiset (0) | 2022.12.26 |