오른값 참조(rvalue reference)
c++11에서 가장 유의미한 변화를 준 것은 오른값 참조이다. 코드의 속도가 달라질 정도로 많이 변화가 되었다.
lvalue는 단일식을 넘어서 계속 지속되는 개체를 의미하고
rvalue는 lvalue가 아닌 나머지를 의미한다.(임시 값, 열거형, 람다, i++ 등)
#include <iostream>
using namespace std;
class Knight
{
public:
public:
int _hp = 100;
};
// 불필요한 복사가 일어남, 원본 객체 수정 불가
void TestKnight_Copy(Knight knight) {
// TODO
}
// 불필요한 복사가 일어나지 않음. 원본 객체 수정 가능
void TestKnight_LValueRef(Knight& knight)
{
// TODO
}
int main()
{
// 오른값 참조
Knight k1;
TestKnight_Copy(k1);
TestKnight_LValueRef(k1);
TestKnight_LValueRef(Knight()); // 에러나는걸 확인 할 수 있다.
return 0;
}
다음과 같은 상황에서 에러가 나는 모습을 확인 할 수 있다. 그 이유는 Knight()는 우리가 임시로 만들 값이기 때문에 rvalue이다. 하지만 매개변수로는 lvalue를 원하고 있기 때문에 오류가 나는 모습이다.
c++11에서 오른값을 받을 수 있는 문법이 추가되었다.
#include <iostream>
using namespace std;
class Knight
{
public:
public:
int _hp = 100;
};
// 불필요한 복사가 일어남, 원본 객체 수정 불가
void TestKnight_Copy(Knight knight) {
// TODO
}
// 불필요한 복사가 일어나지 않음. 원본 객체 수정 가능
void TestKnight_LValueRef(Knight& knight)
{
// TODO
}
void TestKnight_RValueRef(Knight&& knight)
{
// TODO
knight._hp = 120;
}
int main()
{
Knight k1;
TestKnight_Copy(k1);
TestKnight_LValueRef(k1);
// 오른값 참조
TestKnight_RValueRef(Knight());
TestKnight_RValueRef(static_cast<Knight&&>(k1));
// 일반적으로 오른값을 넘겼다는건은 원본을 유지할 필요가 없다 라는 힌트를 준다!!
return 0;
}
그렇다면 오른값참조를 사용해야하는 이유는 무엇인가.
데이트를 복사하는 것이 아닌 이동할때 유리하게 동작한다. move를 이용해 오른값 참조를 할 수 있다.
한마디로 원본을 유지할 필요가 없을 때 사용한다.
예를 들어 플레이어 A, B 가 있고 A 플레이어만 펫을 가지고 있다고 해보자. 그리고 플레이어가 죽었을 때, 다음 플레이어에게 펫을 건네주는 로직이 있다고 해보자.
우리가 오른값 참조를 사용하지 않는다면 A가 죽었을 때 B = A; 꼴로 복사 대입 연산자를 호출하게되고 그 연산자 내부에서 깊은 복사(펫을 포인터로 들고 있다고 가정)를 진행해 펫을 새로 동적할당한 후 펫 내부에서 복사 생성자를 호출할 것이다.
하지만 원본인 A는 죽었으니 쓸모가 없어지게됐다는걸 우리는 알고 있다. 그렇기 때문에 A의 펫을 그냥 그대로 B에게 넘겨주면 된다. 즉 B = std::move(A); 꼴로 복사 대입 연산자를 오른값 참조를 통해 넘겨주게되면 얕은 복사 (pet = A.pet)로도 넘겨줄 수 있게 된다.
전달 참조(Forwading reference)
오른값 참조와 매우 비슷하게 생긴 문법이라 착각할 수 있다.
#include <iostream>
using namespace std;
class Knight
{
public:
Knight() { cout << "기본 생성자" << endl; }
Knight(const Knight&) { cout << "복사 생성자" << endl; }
Knight(Knight&&) noexcept { cout << "이동 생성자" << endl; }
public:
int _hp = 100;
};
void Test(Knight k)
{
}
template<typename T>
void Test_forwardingRef(T&& param)
{
// 왼값 참조라면 왼값으로 건네주고
// 오른값 참조라면 오른값으로 건네줌
Test(std::forward<T>(param));
}
int main()
{
Knight k1;
Test_forwardingRef(std::move(k1)); // 오른값 참조
Test_forwardingRef(k1); // 왼값 참조
// 이상하게도 둘다 통과가 됨.
auto&& k2 = k1; // 왼값 참조
auto&& k3 = std::move(k1); // 오른값 참조
// 이상하게도 둘다 통과가 됨.
// 공통점 : 형식 영역(auto, template)가 일어날 때
// 전달 참조는 형식 영역에서만 동작함.
// 또한 && 앞에 다른 키워드가 붙는다면 전달참조로 동작안함.
// const T&& param ... 오른값 참조로 동작
return 0;
}
'C++ > 기초' 카테고리의 다른 글
[Modern C++] 람다(lambda), 스마트 포인터 (1) | 2023.12.06 |
---|---|
[Modern C++] using, enum class, delete(삭제된 함수), override, final (0) | 2023.12.06 |
[Modern C++] auto, 중괄호 초기화, nullptr (1) | 2023.12.05 |
[C++] STL - deque, map, set, multimap, multiset (1) | 2023.12.05 |
[C++] STL - List (0) | 2023.12.04 |