프로그래밍/C++

#66. 오른값참조 ( rvalue reference )

코딩하는상후니 2022. 7. 22. 22:39

 


 

 

*왼값 ( l-value )

 

=> 단일식을 넘어서 계속 지속되는 개체
 

 

 

*오른값 ( r-value )

 
=> lvalue 가 아닌 나머지 개체 ( 임시 값, 열거형, 람다, i++ 등.. )
 
즉,
좌측값은 & 연산자를 통해 어떠한 메모리 위치를 가리킬 수 있다.
우측값은 그렇지 않은 값들이다.
 
 
 

* 왼값 / 오른값 구별

 

int test = 20;
test = 123 + test;

 

test : 왼값.
123, 20 : 오른값.
123 + test : 오른값.

 

 

 

int result = func();

 

func 의 리턴값 : 오른값.
result : 왼값.

 

 

 


 

 

*이전의 상황 ( 오른값 참조가 없었을 때 )

 

void TestKnight_Copy(Knight k) {}
void TestKnight_LValueRef(Knight& k) {}
void TestKnight_ConstLValueRef(const Knight& k) {}

TestKnight_ConstLValueRef(Knight());
 
=> const 만 오른값을 받을 수 있었음. => const 라서 데이터를 접근할 수 없어 아쉽다.
 
C++11 에 오면서 이런 오른값을 받을 수 있는 참조 방법이 생김 !

 

 

 

 

 

 

 

 

 

* 이동 생성자

 

void TestKnight_RValueRef(Knight&& k)
{
	 k.Print();
}
TestKnight_RValueRef(Knight());

 

// 이동 생성자
Knight(Knight&& other) { ... }

// 이동 대입 연산자
Knight& operator=(Knight&& other)
{
	Hp = other.Hp;
	pet = other.pet;
	other.pet = nullptr;  => null 로 해주지 않으면 나중에 소멸자에서 ERROR
}
=> 객체의 데이터를 단지 이동한다. 강탈해가는 느낌.
 
들어오는 데이터를 사용하지 않는 데이터 ( ex.임시객체 ) 라고 인지할 수 있음.
따라서, 그 원본 데이터를 마음대로 조작해도 된다는 의미.

 

 

 

 

 

 

*std::move, static_cast<X&&>

 

 

void foo(X&& x)
{
    X anotherX = x;
}
 
=> X 클래스의 복사, 이동 생성자 , 대입 연산자 가 정의되어있다고 가정할 때,
위 코드 anotherX 에 x 를 대입하는 곳에선 복사생성자 호출.
 
 
 
즉,
매개변수로 들어오기전에 '오른값' 이었어도
오른값이 이름을 가진다면 '왼값' 으로 변할 수 있다.
 
 
 
 
 
 
* 이 때, 좌측값을 우측값으로 쓰고 싶다면??
 
 
=> 이 때, std::move 사용.
 
 
void foo(X&& x) 
{ 
    X anotherX = std::move(x); 
}
=> 이동 생성자 호출.
 
std::move(x);
static_cast<X&&>(x);

=> 이 두 문장은 같은 뜻이다.

 

 

 

 

 

 

 

 

 

 

*컴파일러 최적화

 

X Func()
{
    X x;

    //return x;
    return std::move(x);
}

 

=> X 클래스의 복사, 이동 생성자 , 대입 연산자 가 정의되어있다고 가정할 때,
 
X 를 리턴할 때 복사 생성자를 호출하므로,
 
move 를 이용해 이동생성자로 넘겨주고 싶을 것이다.
 
 
 
 
하지만, 굳이 std::move 를 쓰지 않아도 같은 방식으로 동작한다.
 
( 컴파일러가 똑똑해져 알아서 최적화해줌. )

 

 

 

중요한 것은
 
이동생성자를 정의해주어야 한다는 것.
만약 정의되지 않았다면 복사생성자가 호출됨.
 
std::move( ) 는 이동생성자가 정의되어있어야지만 기대하는 효과가 있음.

 

 

 

 

 

 

 

 


 

*noexcept

 
=> 함수 선언 시, 해당 함수가 예외를 방출하지 않을 것임을 컴파일러에게 명시.
 

 

*이동 생성자 옆에 noexcept 를 붙이는 이유.
=> 컴파일러가 혹시라도 모를 예외를 생각해 이동생성자를 호출하지 않고
복사생성자를 호출할 수 있는 상황이 발생할 수 있으므로,
noexcept 키워드로 '예외상황은 없다' 라고 컴파일에게 알려주는 용도.
 
 
 
ex)
vector 의 메모리 재할당을 예로 들 수 있다.
vector 에선  push_back 시 capacity 를 초과하면 메모리 재할당을 하게 되는데
이 때, 새로운 메모리를 할당하고 기존의 메모리에서 새로운 메모리에 데이터를 복사하는 과정에서,
새로운 메모리에 데이터를 '복사' 하게 되면 예외가 발생해도 문제가 없다.
왜냐 ?? 아직 기존의 데이터는 남아있다. 데이터가 모두 복사 되고 난 후에야 기존 메모리를 지우기 때문.
하지만, 데이터를 '이동' 시키게 되면
데이터가 옮겨짐과 동시에 기존의 데이터도 같이 수정되어진다.
이 때 예외가 발생한다면 ??
컴파일러는 난감하다.
기존의 데이터도 회손되었고 그렇다고 새로운 메모리에 데이터도 모두 채워지지 않았다.
이런 상황을 방지하기 위해 컴파일러는 이동생성자보다 복사생성자를 더 선호한다.
'가능하면 이동하되 필요하면 복사한다' 라는 마인드이다.
물론, noexcept 선언 하고 정말로 그 때 예외가 발생해서 프로그램이 뻗을 수 있겠지만,
게임 개발에 있어서 예외는 곧 크래시이다.
 

 

 

 


 

 

 

참고 링크

 

 

 

 

본 내용은 인프런의 루키스님 강의를 듣고 정리한 내용입니다.
 

 

 

 

'프로그래밍 > C++' 카테고리의 다른 글

#68. 람다 (lambda)  (0) 2022.07.22
#67. 전달 참조 ( forwarding reference )  (0) 2022.07.22
#65. enum class & override & final  (0) 2022.07.22
#64. using  (0) 2022.07.22
#63. nullptr  (0) 2022.07.22