rvalue reference
오른값 참조는 실제로 많이 사용하진 않지만 C++11 에서 가장 큰 변화라고 할 수 있다.
rvalue의 사용으로 이전 C++과는 엄청난 속도차이를 불러일으켰다.
왼값 (lvalue) vs. 오른값(rvalue)
lvalue : 단일 식을 넘어서 계속 지속되는 개체
rvalue : lvalue가 아닌 나머지(임시값, 열거형, 람다, i++)
왜 왼값과 오른값이라는 표현을 사용하는지.
int a = 3;
a = 4;
cout<<a<<endl;
test(a);
이렇게 처음에 사용한 '왼쪽'에 있던 a는 계속 사용할 수 있다.
반면 3 = a; 이렇게 사용할 수 없다.('식이 사용할 수 있는 왼값이여야합니다.' 라고 오류가 난다.)
또 a++ = 5; 이렇게도 사용할 수 없다.
이런 3이나 a++같은 임시적으로만 사용할 수 있는 값을 통상적으로 오른값이라고 한다.
그런데 기본적인 상수가 아니라
hp가 100인 Knight클래스를 정의한다음 k1이라는 객체를 만들었다고 하자.
class Knight
{
public:
int _hp = 100;
public:
void PrintInfo() { cout<<_hp<<endl; }
void PrintInfoConst() const { cout<<_hp<<endl; }
}
void TestKnight_Copy(Knight knight) {}
void TestKnight_LValueRef(Knight& knight) {}
void TestKnight_ConstLValueRef(const Knight& knight)
{
//knight.PrintInfo();
knight.PrintInfoConst(); // const로 받은 인자의 const함수만 사용할수 있다.
}
int main()
{
Knight k1;
TestKnight_Copy(k1); // 만약 Knight 클래스의 크기가 크면 복사하는 방식이므로 좋지 않다.
TesetKnight_LValueRef(k1); // k1의 주소값을 넘겨줘 불필요한 복사도 없고 원본을 넘길수 있다.
TesetKnight_LValueRef(Knight());
// 임시객체를 넘기려고 하면 const가 붙지않은 값은 lvalue여야한다고 오류가 뜬다.
// 즉, 단일식을 넘어서 계속 지속되는 개체가 아니라 오류가 나는데
TestKnight_ConstLValueRef(Knight());
// 이렇게 인자를 const로 받으면 임시객체를 넘기는게 된다.
// const가 붙지 않으면 함수 내에서 인자로 넘어온 변수의 값을 바꿀 수 있는데
// 사실상 임시개체가 들어오면 사라지므로 의미 없는 계산이다.
// const는 읽기용으로 사용할 것이라 예측하기 때문에 넘어간다.
return 0;
}
이전까지는 위와 같았는데 C++11 이후엔 오른값을 넘기는 방법이 생겼다.
void TestKnight_RValueRef(Knight&& knight) {}
//&&를 두 개 붙여서 오른값을 참조로 받을 수 있다.
int main()
{
Knight k1;
//TestKnight_RValueRef(k1); // lvalue는 받을 수 없어 오류가 난다.
TestKnight_RValueRef(Knight()); 이렇게 다시 오른값을 보내면 잘 실행된다.
return 0;
}
디스어셈블리를 보면 TestKnight_Copy 는 _hp값만 복사되어 k1이 아닌 다른 객체가 전달되고
LValueRef, ConstLValueRef, ValueRef 모두 비슷하게 원본 (Knight()을 넘겨주는곳은 스택에 잠깐 객체를 만든다음) 그 주소를 꺼내서 함수에 넘겨주는 방식이다.
이렇게 오른값을 넘겨서 함수내에서 그 인자를 수정하는게 의미가 있나 싶지만 꼭 임시값만 넣을 수 있는 것은 아니다.
TestKnight_RValueRef(static_cast<Knight&&>(k1);
k1은 실질적으로 존재하지만 RValue로 캐스팅해서 보내면 동작하므로
TestKnight_RValueRef 안에서 k1의 원본 값들을 수정할수 있다.
-> 그럼 lvalue를 인자로 받는 함수를 쓰면 되지 굳이 이걸 쓰는 이유는..?
어셈블리단에서는 LValue를 받는것과 RValue를 받은 것이 큰 의미가 차이가 없지만 C++입장에서 보면 크게 다르다.
함수들의 인자들을 비교해보면
Knight& knight 는 원본을 넘겨주니 수정이 가능하고 마음대로 다룰 수 있다.
const Knight& knight 는 원본을 넘겨주나 수정해선 안되고 읽는 것은 가능하다.
Knight&& knight 는 원본 객체를 넘겨주니 자유롭게 읽고 쓰기가 가능하고 심지어 인자로 넘긴 후 원본은 더 이상 활용하지 않을것이니 마음대로 다룰 수 있다. 즉, 이동대상이라는 의미이다.
원본을 유지하지 않아도 되면 생기는 장점을 생각해보면
만약 Knight클래스의 크기가 엄청 큰 클래스라고 가정해보자.
class Pet
{
};
class Knight
{
public:
Pet* _pet = nullptr;
int _hp = 100;
public:
Knight() {cout<<"Knight()"<<endl;}
// 복사 생성자.
Knight(const Knight& knight)
{
cout<<"const Knight&"<<endl;
}
// 이동 생성자. (오른값을 받는다.받은 값을 보내고 나서 더 이상 활용하지 않는다.)
Knight(Knight&& knight)
{
cout<<"Knight&& knight)"<<endl;
}
~Knight()
{
if(_pet)
delete _pet;
}
public:
// 복사 대입 연산자
void operator=(const Knight& knight)
{
cout<<"operator=(const Knight&)"<<endl;
_hp = knight._hp;
//_pet = knight._pet; // 얕은 복사의 문제점. knight의 _pet을 나도 공유하게 된다.
if(knight._pet != null)
_pet = new Pet(*knight._pet); // 깊은 복사. 그런데 새로 만들어야하므로 비싸다.
}
// 이동 대입 연산자 - 받은 값을 더이상 사용하지 않으므로 훼손해도 된다.
void operator=(Knight&& knight)
{
_hp = knight._hp;
_pet = knight._pet; // 얕은 복사를 사용해도 어차피 knight가 없어질 것이므로 바로 _pet을 사용해도 된다..!
knight._pet = nullptr;
}
};
int main()
{
/// ....
Knight k2;
k2._pet = new Pet();
k2._hp = 1000;
Knight k3;
k3 = static_cast<Knight&&>(k2); // k2가 오른값이므로 이동대입연산자가 호출된다.
// k2는 더이상 사용하지 않는다는 정보를 같이 주는 것이다.
// k2의 값을 k3가 뺏어오니깐 k2의 값들을 '이동'시키는 것이다.
// 참고로 static_cast로 오른값으로 타입변환을 하기도 하지만 일반적으로
k3 = std::move(k2); // 이렇게 사용한다. move가 오른값 참조로 캐스팅한것과 마찬가지로 내부에서 동작한다.
return 0;
}
복사생성자나 복사대입 연산자에서 깊은 복사를 사용하면 새로운 객체를 만들어야하므로 더 느리고 무거울 수 있는데
이동생성자나 이동대입연산자에서는 전달받은 객체를 더 이상 사용하지 않을 것을 아므로 얕은 복사만으로 이전 정보들을 빠르게 가져와 쓸 수 있다.
std::move()를 사용해 오른값 참조로 캐스팅을 주로 하는데 처음에 만들 때 rvalue_cast라는 이름으로 쓸까도 후보에 있었다곤 한다. 코어에서는 임시값을 사용했다가 쓰는 일이 빈번해서 C++11 이후에는 성능차이가 크게 생겼다.
또 다른 사용 예시를 보자면
Knight* k; 를 많이 쓰다보면 누가 k를 관리하는지 확인하기 쉽지 않다보니 딱 하나만 사용한다고 하면 unique_ptr을 사용할 수 있다. 프로젝트에 딱 하나만 존재해야하는 포인터
std:: unique_ptr<Knight> uptr = std::make_unique<Knight>(); 를 만든 다음에
//std:: unique_ptr<Knight> uptr2 = uptr; // 하면 삭제된 함수라고 오류가 난다. unique_ptr 클래스를 설계할 때 복사부분을 다 제거한 것이다.
만약 이 시점에서 uptr을 더 이상 사용하지 않을 심산으로 uptr2에게 uptr을 관리해달라고 넘기고 싶어지면
unique_ptr의 소유권을 넘긴다는 뜻으로 오른값으로 캐스팅하는 std::move를 사용해
std:: unique_ptr<Knight> uptr2 = std::move(uptr);
이렇게도 오른값 참조를 사용할 수 있다.
즉, 오른값 참조는 원본값을 더 이상 사용할 필요가 없다거나 unique_ptr처럼 권리를 넘기고 싶을 때 사용하면 된다. 이로써 복사하는 값이 들지 않고 포인터를 이동으로 넘겨줌으로써 속도에 엄청난 이점을 가져왔다.
'[C++]' 카테고리의 다른 글
[C++ 20] Module (1) | 2024.11.13 |
---|---|
Modern C++ #9 전달 참조(forwarding reference) (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 |
STL#3 Deque (0) | 2022.12.22 |