*왼값 ( 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 |