본문 바로가기

Information Technology/C++

[C++] std::move(1)

개인의 학습을 목적으로 정리한 글입니다. 이점 참고하고 읽어주세요 ;)


#pragma once
#include <iostream>

template<class T>
class AutoPtr
{
public:
	T* m_ptr;

public:
	AutoPtr(T* ptr = nullptr)
		:m_ptr(ptr)
	{
		std::cout << "AutoPtr default constructor" << std::endl;
	}

	~AutoPtr()
	{
		std::cout << "AutoPtr desturctor" << std::endl;
		if (m_ptr != nullptr) delete m_ptr;
	}

	AutoPtr(const AutoPtr& a) // L-value reference
	{}

	AutoPtr& operator =(const AutoPtr& a)
	{}

	AutoPtr(AutoPtr&& a) // R-value
		: m_ptr(a.m_ptr)
	{}

	AutoPtr& operator =(AutoPtr&& a) // 지울 것이기 때문에!
	{}
};

AutoPtr 클래스의 전체 코드를 올리기엔 가독성도 떨어질 것 같아서 전체적인 틀만 보여드렸습니다.

훑어보시면 AutoPtr클래스에서 L-value를 사용하는 복사 생성자와 대입 연산자와 함께

R-value를 사용하는 복사 생성자와 대입 연산자도 멤버로 가지고 있는 것을 알 수 있습니다.

#include "AutoPtr.h"
#include "Resource.h"
using namespace std;

int main()
{
	{
		AutoPtr<Resource> res1(new Resource(10000000));

		cout << res1.m_ptr << endl;

		AutoPtr<Resource> res2 = res1;

		cout << res1.m_ptr << endl;
		cout << res2.m_ptr << endl;
	}
}

AutoPtr.h 헤더 파일과 Resource.h 헤더 파일을 include 하여 사용하고 있는 main함수를 보시면,

AutoPtr 자료형으로 res1을 선언한 다음,

똑같이 AutoPtr 자료형인 res2를 선언함과 동시에 res1을 대입하고 있습니다.

이 경우에는 AutoPtr클래스에서 L-value를 사용하는 복사 생성자를 호출하게 되어 그 과정에서 AutoPtr과 Resource를 한 번씩 더 호출하게 됩니다. 하지만

AutoPtr<Resource> res2 = std::move(res1);

res2에 res1로 초기화할 때, std::move()로 res1을 묶게 되면,

res1을 L-value가 아닌 R-value로 여기게 되고 AutoPtr클래스에서 역시 R-value를 매개변수로 받는 복사 생성자를 호출합니다! 

=을 사용하는데 왜 대입 연산자가 아니라 복사 생성자인가요?라고 혹시 생각하신다면,

res2의 초기화가 완료된 상태에서 res1을 대입하는 것이 아니라,

res2를 초기화하는 과정이기 때문에 대입 연산자가 아니라 복사 생성자가 호출됩니다.

그래도 헷갈리신다면

AutoPtr<Resource> res2(std::move(res1));

이 코드가 위에 있는 코드와 동일하다는 걸 생각해주세요! 실행 결과 역시 동일합니다. 

그리고 출력 결과를 보면

R-value를 사용하는 move-semantics에 의해서, res2가 res1의 주소를 그대로 이어받는 걸 알 수 있습니다.

 

주의하실 점은

res2가 res1의 포인터 주소를 넘겨받으면서, res1에는 어떤 주소도 남아있지 않습니다.

때문에 프로그램에서 위의 예제에 나오는 res1과 같이 앞으로 더 이상 사용하지 않는 객체 혹은 변수일 경우에만 R-value로 사용해야 합니다.

그렇지 않고 프로그램상 나중에 또 사용해야 하는 데 R-value로 사용한다면, 이미 그 값은 사라져 버린 뒤라 문제가 발생하게 됩니다.