C++ 중급 - Move Semantics

타입·2024년 2월 22일
0

C++ 공부

목록 보기
14/17

Move 개념

메모리 복사가 아닌 자원의 이동, 원래 가지고 있던 포인터는 nullptr로 초기화

#include <string>
int main()
{
	std::string s1 = "Practice make perfect";
	std::string s2 = s1;

	std::string s3 = "Practice make perfect";
	std::string s4 = std::move(s3);

	std::cout << s1 << std::endl;
	std::cout << s2 << std::endl;
	std::cout << s3 << std::endl; // ""
	std::cout << s4 << std::endl;
}

  • std::move() 장점
    swap1()은 메모리 복사가 3번이 일어나고
    swap2()는 주소 복사가 3번 일어나 더 빠름
#include <string>

template<class T>
void swap1(T& lhs, T& rhs)
{
	T tmp = lhs;
	lhs = rhs;
	rhs = tmp;
}

template<class T>
void swap2(T& lhs, T& rhs)
{
	T tmp = std::move(lhs);
	lhs = std::move(rhs);
	rhs = std::move(tmp);
}

int main()
{
	std::string s1 = "Practice make perfect";
	std::string s2 = "To be or not to be";

	swap1(s1, s2);
}
  • move 개념을 통해서 알아야 하는 것들
  1. swap 같은 알고리즘을 작성 할 때 std::move()를 사용하면 성능향상을 볼 수 있음
  2. 사용자가 만든 타입이 std::move()를 지원하게 하는 방법 - move constructor 개념
    + move 개념을 활용할 때 주의사항

Move Constructor

컴파일러가 만든 복사 생성자는 얕은 복사로 진행
사용자가 복사 생성자를 만들어 깊은 복사하여 해결 가능
하지만 성능상의 문제가 존재

#include <cstring>

class Person
{
	char* name;
	int   age;
public:
	Person(const char* s, int a) : age(a)
	{
		name = new char[strlen(s) + 1];
		strcpy_s(name, strlen(s) + 1, s);
	}
	~Person() { delete[] name; }

	Person(const Person& p) : age(p.age)
	{
		name = new char[strlen(p.name) + 1];
		strcpy_s(name, strlen(p.name) + 1, p.name);	
	}
};
Person foo()
{
	Person p("john", 20);
	return p; // 함수가 객체를 값으로 반환하면 임시객체를 반환
}
int main()
{
	Person ret = foo(); // 임시객체이므로 바로 파괴됨
}

메모리를 복사하지 말고 주소를 복사하면 보다 효율적일 것으로 예상
임시객체를 위한 복사 생성자를 추가로 만들자!

  • 임시객체를 위한 복사 생성자
    복사 생성자, move 생성자 둘 다 만들면 rvalue는 우선순위에서 인자가 Person&&의 함수를 실행
class Person
{
	...
    // lvalue와 rvalue를 모두 받을 수 있음
	Person(const Person& p) : age(p.age) // 복사 생성자
	{
		name = new char[strlen(p.name) + 1];
		strcpy_s(name, strlen(p.name) + 1, p.name);	
	}
    // rvalue만 받을 수 있음
	Person(Person&& p) : name(p.name), age(p.age) // move 생성자
	{
		p.name = nullptr;	
	}
};
int main()
{
	Person robert("robert", 30);

	Person p1 = robert;
	Person p2 = foo();
}

std::move()

move() 함수에선 rvalue 캐스팅만 해주고 실제 자원 이동은 move 생성자에서 진행

class Object
{
public:
	Object() = default;
	Object(const Object& obj) { std::cout << "copy ctor" << std::endl;}
	Object(Object&& obj)      { std::cout << "move ctor" << std::endl;}
};

Object foo() 
{
	Object obj;
	return obj;
}
int main()
{
	Object obj1;
	Object obj2 = obj1;	 // copy
	Object obj3 = foo(); // move
	Object obj4 = static_cast<Object&&>(obj1); // move
	Object obj5 = std::move(obj2); // move
}
  • std::move()를 사용했는데 클래스에 move 생성자 구현이 없다면
    복사 생성자를 사용
    move 생성자가 있다면 최적화 되지만 없더라도 오류는 발생하지 않음

  • std::move() 구현

#include <type_traits>

class Object
{
public:
	Object() = default;
	Object(const Object& obj) { std::cout << "copy ctor" << std::endl;}
	Object(Object&& obj)      { std::cout << "move ctor" << std::endl;}
};

template<class T> 
constexpr std::remove_reference_t<T>&& move(T&& obj) noexcept
{
	return static_cast<std::remove_reference_t<T> &&>(obj);
}

int main()
{
	Object obj1;
	Object obj2 = obj1;	 	  // copy
	Object obj3 = move(obj1); // move
	Object obj4 = move(Object()); // move
}

move() 함수는 lvalue와 rvalue 모두 받을 수 있도록 유연하게 설계하는 것이 좋음
그래서 move(T&& obj)로 선언했다면 reference collapsing 규칙에 의해 static_cast<T&&>(obj)에 lvalue가 들어왔을때 rvalue 캐스팅이 되지 않고 lvalue를 반환하여 move 생성자가 아닌 복사 생성자를 호출해버림;
레퍼런스 속성을 없애주는 std::remove_reference_t()를 사용하여 move() 함수를 구현해야함


자동 생성 규칙

복사 생성자와 move 생성자의 자동 생성 규칙

class String
{
public:
	String() = default;
	String(const String& obj) 		 { std::cout << "String copy ctor" << std::endl;}
	String(String&& obj)      		 { std::cout << "String move ctor" << std::endl;}
	String& operator=(const String&) { std::cout << "String copy assignment" << std::endl; return *this;}
	String& operator=(String&&)      { std::cout << "String move assignment" << std::endl; return *this;}
};

class Object
{
	String name;
public:	
	Object() = default;
	
	Object(const Object& obj) : name(obj.name) {}
	Object& operator=(const Object& obj) { name = obj.name; return *this;}
	Object(Object&& obj) 	  : name(std::move(obj.name)) {}
	Object& operator=(Object&& obj) { name = std::move(obj.name);return *this;}
};

int main()
{
	Object obj1;
    
	Object obj2 = obj1;
	obj2 = obj2;

	Object obj3 = std::move(obj1);
	obj3 = std::move(obj1);
}
  1. 사용자가 복사 계열과 move 계열 함수를 모두 제공하지 않을 때

  2. 사용자가 복사 생성자(또는 복사 대입 연산자)만 제공하는 경우

    복사 생성자만 제공한 경우 복사 대입 연산자를 컴파일러가 제공하는 것이 과연 바람직한 것일까 - C++11때 잘못된 설계란 걸 알게 됨
    (복사 생성자를 사용자가 구현했다는 건 복사 대입 방법 또한 디폴트와 달라질 거란 의미, 사용자가 복사 대입 연산자까지 구현하는 것이 맞음)

  3. 사용자가 move 생성자(또는 move 대입 연산자)만 제공하는 경우

    move 대입 연산자가 없으면 복사 대입 연산자를 사용하는데 복사 대입 연산자가 없으므로 에러 (삭제됨과 제공 안함을 구분)

  • 복사 생성자는 사용자가 제공하고, move 계열 함수는 컴파일러에게 요청하고 싶으면
    move 계열 함수를 '= default'로 요청
    이러면 자동 생성 규칙에 따라 복사 대입 연산자는 제공되지 않으므로 복사 대입 연산자도 '= default'로 함께 요청
class Object
{
	String name;
public:	
	Object() = default;
	
	// 복사 생성자 제공
	Object(const Object& obj) : name(obj.name) {}

	Object(Object&& obj) = default;
	Object& operator=(Object&& obj) = default;
	Object& operator=(const Object& obj) = default;
};
  • 클래스 내부적으로 포인터 멤버가 있고 동적메모리 할당 등을 하는 코드가 있다면
    • C++98 시절
      소멸자, 복사 생성자, 복사 대입 연산자 구현 필요
      Rule Of 3
    • C++11 이후
      소멸자, 복사 생성자, 복사 대입 연산자, move 생성자, move 대입 연산자 구현 필요
      Rule Of 5
#include <cstring>

class Person
{
	char* name;
	int   age;
public:
	Person(const char* s, int a) : age(a)
	{
		name = new char[strlen(s) + 1];
		strcpy_s(name, strlen(s) + 1, s);
	}
	~Person() { delete[] name; }

	Person(const Person& p) : age(p.age)
	{
		name = new char[strlen(p.name) + 1];
		strcpy_s(name, strlen(p.name) + 1, p.name);
	}

	Person& operator=(const Person& p)
	{
		if (this == &p) return *this;

		age = p.age;
		delete[] name;
		name = new char[strlen(p.name) + 1];
		strcpy_s(name, strlen(p.name) + 1, p.name);

		return* this;
	}
	Person(Person&& p) noexcept 
			: name(p.name), age(p.age)
	{
		p.name = nullptr;
	}

	Person& operator=(Person&& p) noexcept
	{
		if ( this == &p) return *this;
		
		delete[] name;
		age = p.age;
		name = p.name;
		p.name = nullptr;

		return* this;
	}
};
int main()
{
	Person robert("john", 30);
}
  • Rule Of 0
    char* 대신 std::string을 사용하면 사용자가 직접 자원을 관리할 필요없음
    컴파일러가 모두 자동 생성
#include <string>

class Person
{
	std::string name;
	int   age;
public:
	Person(const std::string& s, int a) : name(s), age(a)
	{
	}
};
int main()
{
	Person robert("john", 30);
}

STL을 잘쓰자!

profile
주니어 언리얼 프로그래머

0개의 댓글