프로그래밍/C/C++

C++ rvalue reference (우측값 참조) move semantics

nanze 2022. 1. 6. 11:58
반응형

이전 정리글은 

2022.01.05 - [프로그래밍/C/C++] - C++ Lvalue (좌측값) Rvalue (우측값) Rvalue reference (우측값 참조자)

 

C++ Lvalue (좌측값) Rvalue (우측값) Rvalue reference (우측값 참조자)

c++ lvalue 와 rvalue lvalue 와 rvalue 라는 무엇인가 ? 말그대로 좌측값과 우측값인데 c++ 에서 의미는 다음과 같이 생각하면 된다. lvalue 같은 경우에는 표현식 이후에도 사라지지 않고 지속되는 변수 또

nanze.tistory.com

move semantics

move semantics 는 이전 정리에서도 말했지만 객첵의 자원이 다른 객체의 자원으로 이동하는 것을 의미한다. 우측값 참조는 이러한 메카니즘을 가능하게 하고 이것은 경우에 따라 성능을 매우 향상 시킨다. 임시 객체의 자원 이동 간에 불필요한 메모리 할당이나 복사 연산을 생략함으로써 처리 시간이 줄어드는 것이다. 역시 코드를 보는 것이 빠를지 모르겠다. 

#include <iostream>
#include <vector>
using namespace std;

class clsObject
{
public:
	clsObject(size_t sz) {
		mSize = sz;
		mData = new uint8_t[sz];
	}
	~clsObject() {
		if (mData != nullptr) {
			delete[] mData;
		}
	}
	
	// 복사 생성자
	clsObject(const clsObject& cls) {
		wcout << "복사 생성자 호출" << endl;
		mSize = cls.mSize;
		mData = new uint8_t[mSize];
		copy(cls.mData, cls.mData + cls.mSize, mData);
	}

	// 대입 연산자
	clsObject& operator= (const clsObject& cls) {
		wcout << "대입 연산자 호출" << endl;
		if (this != &cls) {
			delete[] mData;
			mSize = cls.mSize;
			mData = new uint8_t[mSize];
			copy(cls.mData, cls.mData + cls.mSize, mData);
		}
		return *this;
	}

private:
	size_t mSize;
	uint8_t* mData;
};

int main()
{
	vector<clsObject> vecTest;
	vecTest.push_back(clsObject(13));
	vecTest[0] = clsObject(31);

	return 0;
}

위 코드를 보면 벡터에 push_back 하려고 할때 clsObject(13) 에 의해서 임시 객체가 생성되고 clsObject 의 복사 생성자가 호출되며 vecTest[0] 의 대입하려고 할 경우에도 임시객체가 생성되고 대입 연산자가 호출되게 된다. 각각의 복사 생성자와 대입 연산자는 인자로 들어오는 객체를 복사하기 위하여 메모리를 할당하고 각 원소를 복사하고 있다. 이 과정없이 임시객체의 값을 가르키게 만들면 얼마나 좋은가

그것을 가능하게 하는것이 move semantics 이며 관련 함수는 다음과 같다. 

move 생성자 & move 대입 연산자

rvalue 즉 임시 객체나 값으로부터 데이터 이동을 시킬 경우 복사가 아닌 바로 참조시키도록 하는 것은 객체의 move 생성자와 move 대입 연산자를 구현함으로써 가능하다. 그리고 이것의 방법은 우측값 참조를 쓰는것이다. 

코드를 보자. 

#include <iostream>
#include <vector>
using namespace std;

class clsObject
{
public:
	clsObject(size_t sz) {
		mSize = sz;
		mData = new uint8_t[sz];
	}
	~clsObject() {
		if (mData != nullptr) {
			delete[] mData;
		}
	}
	
	// 복사 생성자
	clsObject(const clsObject& cls) {
		wcout << "복사 생성자 호출" << endl;
		mSize = cls.mSize;
		mData = new uint8_t[mSize];
		copy(cls.mData, cls.mData + cls.mSize, mData);
	}

	// 대입 연산자
	clsObject& operator= (const clsObject& cls) {
		wcout << "대입 연산자 호출" << endl;
		if (this != &cls) {
			delete[] mData;
			mSize = cls.mSize;
			mData = new uint8_t[mSize];
			copy(cls.mData, cls.mData + cls.mSize, mData);
		}
		return *this;
	}
    
    // move 생성자
    clsObject(clsObject&& cls){
        wcout << "move 생성자 호출" << endl;
        mSize = cls.mSize;
        mData = cls.mData;
        cls.mData = nullptr;
        cls.mSize = 0;
    }
    
    // move 대입 연산자
    clsObject& operator = (clsObject&& cls){
        wcout << "move 대입 연산자 호출" << endl;
        if (this != &cls) {
            delete [] mData;
			mSize = cls.mSize;
			mData = cls.mData;
			cls.mSize = 0;
            cls.mData = nullptr;
		}
		return *this;
    }

private:
	size_t mSize;
	uint8_t* mData;
};

int main()
{
	vector<clsObject> vecTest;
	vecTest.push_back(clsObject(13));
	vecTest[0] = clsObject(31);

	return 0;
}

상위 코드는 이전 코드에 move 생성자와 move 대입 연산자를 구현하였다. '&&' 이 표시가 우측값 참조를 의미한다. 이렇게 객체에 move 관련 함수를 구현해 놓으면 vector 는 move 관련 함수가 구현되어 있을 경우 해당 함수를 호출하게 되어있다. 앞선 코드에서는 복사 생성자와 대입 연산자가 호출되었지만 이번 코드에서는 move 생성자와 move 대입연사자가 호출되었음을 알 수 있다. 원소가 적은 대상으로는 그 속도의 차이가 미비하겠지만 원소가 많을 경우에는 성능 차이가 많이 발생한다. 다음 정리시간에는 우측값 참조의 속성에 대해서 알아보도록 하자. 

 

반응형