정진하는중

[복사 생략] 이동 생성자와 이동 대입 연산자

 

모두의 코드를 보고 정리한 메모입니다.

 

참조하시면 큰 도움이 될 것입니다.

https://modoocode.com/227

 

씹어먹는 C++ - <12 - 1. 우측값 레퍼런스와 이동 생성자>

모두의 코드 씹어먹는 C++ - <12 - 1. 우측값 레퍼런스와 이동 생성자> 작성일 : 2018-03-24 이 글은 74104 번 읽혔습니다. 이번 강좌에서는 복사 생략 (Copy elision)우측값 레퍼런스 (rvalue referen ce)이동 생성

modoocode.com

 

1. 이동 생성자 / R-Value Reference

 

좌측 값(L-Value) 

: C++ 표현식 상에서 주소 값을 가지는  값

 

좌측 값은 & 연산자를 통해 주소를 얻을 수 있는 값 또는 변수 등이며, 표현식의 좌우측 모두에 올 수 있습니다.

int b = 1;
int a = b;

 

b는 좌측 값이지만 우측에도 올 수 있습니다.

 

 

 

우측 값(R-Value) 

 : C++ 표현식 상에서 주소 값을 취할 수 없는 값

 

우측 값은 표현식에서 주소 값이 존재하지 않는 임시 값 또는 임시 객체를 포함합니다.

 

우측 값인 임시 객체는 주소를 가지지 않으며 변수 또한 아니다

 

우측 값은 특정 표현식의 평가가 완료될 동안의 생명 주기를 가지며 평가가 완료될 시 즉시 소멸합니다.

 

C++ 언어에서 참조는 좌측 값만 가질 수 있습니다. 

그러나 예외적으로 const 키워드가 붙은 좌측 값 참조에 대해서는 우측 값을 받아 생명 주기를 연장할 수 있습니다.

이를 L-Value 참조로 간주합니다. 

 


C++ 03 이후의 표준에 따르면, 우측 값을 const 참조에 바인딩하면, 임시 객체의 생명 주기가 그 참조가 범위를 벗어날 때까지 연장됩니다. 이는 임시 객체가 함수 호출 중 소멸되는 것을 방지하고, 안전하게 사용할 수 있게 합니다.

( R-Value 참조에 대해서는 후반에 다루며, C++ 11 이후의 표준입니다 )

 

void process(const MyClass& obj) {
    // obj는 const l-value 참조
    std::cout << "Processing object\n";
}

 

만약 obj가 임시 객체를 통해 호출되었다면, obj의 생명 주기는 process 함수가 반환할 때까지 연장됩니다.


 

 

이러한 특성 때문에 객체의 생성 과정에서 임시 객체를 받는다면 L-Value 참조를 통해 복사 생성을 수행합니다.

( 복사 생성자가 const Type& 매개 변수를 가지는 경우)

 

예제를 만들기 위해서는 C++ 17 이상을 지원하는 컴파일러가 자동으로 수행하는 

복사 생략에 대해서 알아야 합니다. 다른 글을 링크하겠습니다. 

 

https://devoteoneself.tistory.com/9

using namespace std;
class Test
{
private:
	int* _arr;
	int _size;
public:
	
	Test(int size) : _arr(new int[size]), _size(size)
	{
		cout << "생성자 - 멤버 동적 할당" << endl;
	}

	Test(const Test& other) : _arr(new int[other._size]), _size(other._size)
	{
		for (int i = 0; i < other._size; i++)
		{
			_arr[i] = other._arr[i];
		}
	}

	~Test()
	{
		cout << "소멸자 - 동적 할당 해제" << endl;
		delete[] _arr;
	}

};

int main() 
{
	Test t1 = Test(3);
}

 

해당 예제를 실행하면 복사 생성자를 통해 t1 객체를 생성할 것 같지만

RVO로 인해 컴파일러는 t1에서 즉시 일반 생성자를 호출합니다.

 

Debug, Release 모두 동일

 

 

NRVO 또한 적용됩니다. 

+ 연산자를 오버로딩을 추가하고 

 

Test operator+(const Test& other)
{
	cout << "+ 연산자 " << endl;
	Test temp(this->_size + other._size);
	int cnt = 0;

	for (int i = 0; i < this->_size; i++)
	{
		temp._arr[i] = this->_arr[i];
		cnt++;
	}
	for (int i = 0; i < other._size; i++)
	{
		temp._arr[cnt++] = other._arr[i];
	}

	return temp;
}

 

Debug, Release 모두 동일

 

이런 식으로 조금은 복잡한 함수를 통해 임시 객체를 Return 받아도 적용됩니다.

 

똑똑한 컴파일러

 

복사 생성은 일어나지 않았습니다.

 

이동 생성자 / R-Value 참조 

 

이런 간단한 예제에서는 복사 생성을 유도할 순 없었지만, 컴파일러에게 맞기지 않고

명시적으로 이동 생성자를 작성하여 이동 생성을 유도할 수 있습니다.

 

만약 STL Vector에 Test 함수를 push_back 하는 상황을 생각해 봅시다.

L-Value 참조로 간주되어 복사 생성이 일어납니다.

 

vector push_back

 

Debug, Release 모두 동일

 

이미 만들어진 Test(3) 임시 객체를 복사 생성하지 말고 이를 즉시 Vector의 원소로 이동 생성할 수 있습니다.

 

Test(const Test& other) 를 통해 임시 객체를 받는다면 

const 키워드를 통해 임시 객체의 수명을 연장하고 수정할 수 없는 L-Value를 참조하고 있기 때문에 

other의 멤버를 수정할 수 없습니다. 

이 때문에 R-Value를 매개변수로 하여 이동 생성하는 생성자를 명시적으로 정의할 수 있습니다.

 

Test(Test&& other) noexcept 
{
	cout << "생성자 - 이동 생성 " << endl;
    
	this->_arr = other._arr;
	this->_size = other._size;

	other._arr = nullptr;
}

 

이미 만들어진 R-Value 임시 객체를 복사하지 말고 생성자로 이동시키는 방식입니다.

또한 임시 객체가 소멸될 때 할당된 메모리를 소멸자에서 해제하지 않도록 nullPtr로 밀어줍니다.

( const Type& other) 형태로 받았다면 수정할 수 없었겠죠.

 

다시 Vector에 Push_back

 

new를 한 번만 호출한 이동 생성

 

지금 예제에서는 딱히 데이터를 넣지 않고, 수행할 복사도 없었지만 

복사할 멤버 또는 데이터가 많았다면 의미 없는 복사를 수행하였을 겁니다.

 

이동 대입 연산자 

생성자가 아닌 이미 만들어진 객체에 대해서도 복사가 아닌 이동의 연산을 수행하게 할 수 있습니다.

 

다만 이동 연산이 수행된다는 것은 기존의 객체를 더 이상 사용하지 않은 것임을 표명합니다.

 

좌측 값인 객체를 우측 값으로 의미를 변경시켜 이동 대입 연산을 수행하게 할 수 있습니다. 

 

std::move()

 

인자로 받은 좌측 값을 우측 값으로 바꿔줍니다.

 

우측 값을 매개 변수로 받아 이동을 수행하는 이동 대입 연산자는 따로 정의해줘야 합니다.

 

Test operator=(Test&& other) noexcept
{
	cout << " 이동 " << endl;

	_arr = other._arr;
	_size = other._size;

	other._arr = nullptr;
	other._size = 0;
}

 

이런 식으로 이동 대입 연산자를 따로 정의하여 사용할 수 있습니다. 

 

또한 이동 생성자와 이동 대입 연산자를 사용하는 사용자 정의 자료형을

STL에서 사용하기 위해서는 반드시 noexcept를 사용하여야 합니다.

 

 


댓글