C++ Chapter 15.4 : std::move

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!


chapter 15. 의미론적 이동과 스마트 포인터

15.4 std::move

  • #include <utility> 해주어야 사용 가능하다.
  • std::move(A)
    • A 를 R-value로 변환하여 이동생 성자 혹은 이동 대입 연산자가 호출되게끔 한다.

🔔 std::move 쓰기 전

📜AutoPtr.h

  • 복사 생성자, 이동 생성자 둘 다 가지고 있는 상황
#include <iostream>
using namespace std;

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

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

	~AutoPtr()
	{
		cout << "AutoPtr destructor" << endl;

		if (m_ptr != nullptr) delete m_ptr;
	}

	AutoPtr(const AutoPtr& a) // 💎복사 생성자💎 
	{
		cout << "AutoPtr copy constructor" << endl;

		// deep copy
		m_ptr = new T;  
		*m_ptr = *a.m_ptr; // 📜Resource.h 의 대입 연산자 호출하여 깊은 복사 수행
			
	}

	AutoPtr& operator = (const AutoPtr& a) // 💎대입 연산자 오버로딩💎
	{
		cout << "AutoPtr copy assignment" << endl;
		
		if (&a == this)
			return *this;

		if (m_ptr != nullptr) delete m_ptr;

		// deep copy
		m_ptr = new T;  
		*m_ptr = *a.m_ptr; // 📜Resource.h 의 대입 연산자 호출하여 깊은 복사 수행
														
		return *this;
	}
	//AutoPtr(const AutoPtr& a) = delete;
	//AutoPtr& operator = (**const** AutoPtr& a) = delete;  

	AutoPtr(AutoPtr&& a)  // 💎이동생성자💎
		:  m_ptr(a.m_ptr) // 소유권 이전
	{ 
		a.m_ptr = nullptr; // 소유권 박탈
	
		cout << "AutoPtr move constructor" << endl;
	}

	AutoPtr& operator = (AutoPtr&& a)  // 💎이동 대입 연산자 오버로딩💎 
	{
		cout << "AutoPtr move assignment" << endl;

		if (&a == this)
			return *this;

		if (m_ptr != nullptr) delete m_ptr; 
		
		m_ptr = a.m_ptr;  // 소유권 이전
		a.m_ptr = nullptr;  // 소유권 박탈

		return *this;
	}

	T& operator *() const { return *m_ptr; }
	T* operator ->() const { return m_ptr; }
	bool inNull() const { return m_ptr == nullptr; }
};

📜main.cpp

#include <iostream>

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

		cout << res1.m_ptr << endl;

		AutoPtr<Resource> res2 = res1; //  복사 생성자가 호출된다. res1은 L-Value니까 

		cout << res1.m_ptr << endl;
		cout << res2.m_ptr << endl;
	}
}
💎출력💎

Resource length constructed
AutoPtr default constructor
0033F510
AutoPtr copy constructor
Resource default constructed
Resource copy assignment
0033F510
0033F5F0
AutoPtr destructor
Resource destroyed
AutoPtr destructor
Resource destroyed

AutoPtr<Resource> res2 = res1; 👉 이동 생성자, 복사 생성자. 둘다 구현되있는 상태에서 어떤게 호출될까?

  • 일단 객체 생성 과정이므로 복사 생성자 or 이동 생성자 호출
  • 복사 생성자가 호출된다.
    • 복사 대상이 되는 res1 은 L-Value 니까 R-value만 받는 이동 생성자는 이를 받을 수 없으므로.
    • 깊은 복사가 이루어 진다.
      • 깊은 복사를 수행하는 Resource의 대입 연산자 호출.
      • res2는 자기만의 새로운 별개의 공간을 할당 받아 그곳에 res1의 내용물들을 복사하였다.
      • res1res2는 별개. 주소가 다르다.
        • 0033F510, 0033F5F0 둘이 주소 다름


🔔 std::move 로 이동 생성자 호출하기

L-value이지만 이동 생성자를 호출하고 싶다면? 👉 std::move를 통해 R-value로 바꿔주면 된다.

📜main.cpp

#include <iostream>
#include <utility> // ✨✨

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

		cout << res1.m_ptr << endl;

		AutoPtr<Resource> res2 = std::move(res1); // 이동 생성자가 호출된다. std::move는 res1을 R-Value 로 바꿔준다. 

		cout << res1.m_ptr << endl;
		cout << res2.m_ptr << endl;
	}
}
💎출력💎

Resource length constructed
AutoPtr default constructor
001FF540
AutoPtr move constructor
00000000
001FF540
AutoPtr destructor
Resource destroyed
AutoPtr destructor
  • AutoPtr<Resource> res2 = std::move(res1);
    • res1을 R-Value로 리턴해준다.
      • std::move 덕에 이동생성자가 호출된다. R-Value 가 전달 되었기 때문에.
        • 깊은 복사 과정 없음. 따라서 Resource 디폴트 생성자 호출과 Resource 대입 연산자는 호출 되지 않는다.
        • 소유권 이전과 박탈res1 포인터는 nullptr로 초기화 하고 res1 포인터가 한때 맡았던 객체를 이젠 res2가 맡게 된다.
          • res2 주소값이 기존의 res1 주소값이 된 것을 볼 수 있음 001FF540
      • 📢 주의할점 !!!
        • 이후에 res1은 nullptr로 초기화 되기 때문에 이걸로 뭘 하려고 하는건 위험함
          • 잘못 사용하면 독이 될 수도.
          • std::move는 프로그래머가 직접 사용하는 것이기 때문에 이 실수는 프로그래머 책임임


🔔 Copy Semantics VS Move Semantics

#include <iostream>
#include <utility> // ✨✨
#include <vector>

int main()
{
	{
		vector<string> v;
		string str = "Hello";

		v.push_back(str);

		cout << str << endl;
		cout << v[0] << endl;

		v.push_back(std::move(str));

		cout << str << endl;
		cout << v[0] << " " << v[1] << endl;
	}
}
💎출력💎

Hello
Hello
                  👈 null 이어서 비워진 부분
Hello Hello

std::vector의 push_back은 L-Value Reference 와 R-Value Reference 에 따라 오버로딩이 되어있음

  • 💙copy semantics 사용
    • string str = "Hello";
      • str은 L-Value다. v.push_back(str); 에서도 L-Value로 받아들이고 있다.
      • 따라서 “Hello” 를 변수로서 받아들이고 있다는 것이다. 이말은 즉 "Hello"는 str이라는 이름의 다른 메모리 공간에 존재 중.
    • v[0] 이라는 새로운 공간을 만들어 str의 내용물을 깊은 복사 해온 것이다.
      • v[0] 과 str은 같은 내용물을 가지고 있지만 별개의 공간이다.
        • 단순히 깊은 복사로 내용물을 복사해온 것이라서 여전히 str이 가리키는 곳에도 “Hello”객체가 있고 v[0]이 가리키는 곳에도 “Hello” 객체가 있다.
  • 💙move semantics 사용
    • v.push_back(std::move(str));
      • R-Value 로서 str를 v[1]에 move 시킴
      • 이말은 즉 '복사 없이' str이 맡았던 "Hello" 객체는 이제 v[1]이 맡게 되고 str은 nullptr로 초기화가 된다는 얘기.
        • str공간에 있던 “Hello” 객체가 복사된 것이 아닌 v[1]로 ‘이동’이 된 것.
    • v[1]은 기존 Hello 객체를 그대로 소유권을 이동받고 str 은 비워지게 된다.


🔔번외) 이동 생성자가 늘 더 빠른 것은 아니다.

move 연산으로 더 성능이 좋아질 수 있는 케이스를 파악하는 것이 중요함

  • 항상 move 연산이 복사보다 더 빠른 것은 아니다.
    • std::string에서는 move 연산을 안쓰는 것이 더 낫다.
      • std::string는 move를 지원하긴 하는데 문자열 길이가 짧은 경우엔 move연산보단 복사가 더 성능이 낫다고 한다.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기