-
[C++] C++의 move semantics (의미론적 이동?)쾌락없는 책임 (공부)/C++ 짜잘이 2022. 8. 7. 15:22반응형
개요
C++에서 move semantics는 소유권을 이전해 주는 것! 제가 듣던 C++ 강의에서 나왔던 이야기입니다. 이전에 syntax는 문법에 맞는가를 이야기하는 것이고 semantics의 경우 내부적인 의미를 이야기합니다.
이 내용은 스마트 포인터를 보게 되면서 더 많이 알아보게 되는데요. 일단 기본적인 RAII 패턴을 지킨 클래스를 보면서 짚어보겠습니다. 저도 잘 머릿속에 잘 정리가 안된 개념이라 짚어가면서 해야 할 것 같네요.
RAII 패턴 + std::move로 보는 move란
#include <iostream> #include <utility> using namespace std; template<typename T> class RAII { public: T* pointer; RAII(T* _pointer) : pointer(_pointer) { cout << "[constructor]\n"; } ~RAII() { if(pointer != nullptr) delete pointer; cout << "[Destructor]\n"; } RAII(const RAII& _raii) { pointer = new T; *pointer = *_raii.pointer; cout << "Copy Constructor\n"; } RAII(RAII&& _raii) : pointer(_raii.pointer) { _raii.pointer = nullptr; cout << "Move constructor\n"; } RAII& operator = (const RAII& _raii) { cout << "Copy assignment\n"; if(&_raii == this) return *this; if(pointer != nullptr) delete pointer; pointer = new T; *pointer = *_raii.pointer; return *this; } RAII& operator = (const RAII&& _raii) { cout << "Move assignment\n"; if(&_raii == this) return *this; if(pointer != nullptr) delete pointer; pointer = _raii.pointer; _raii.pointer = nullptr; return *this; } T* operator *() const { return *pointer; } T& operator ->() const { return pointer; } };
위 RAII 클래스의 경우 생성자에서 객체 포인터를 받아오고 이후 copy / move 에 해당하는 생성자, 대입 연산이 정의된 클래스입니다. 여기서 객체 관리 클래스에 담을 빈 크래스 A를 만들고 copy와 move의 동작을 보겠습니다.
int main() { RAII<A> ex1(new A); RAII<A> ex2 = ex1; cout << ex1.pointer << " " << ex2.pointer << '\n'; RAII<A> ex3 = std::move(ex1); cout << ex1.pointer << " " << ex3.pointer << '\n'; }
위 ex2 가 만들어 질 때는 Copy Constructor가 만들어지고 ex1과 ex2의 pointer 주소값도 다르게 나옵니다. 반면 아래 std::move를 사용한 문구에서는 기존 ex1의 pointer 주소값이 0으로 되고 ex1의 주소값이 ex3으로 옮겨지게 되었습니다.
위 std::move의 경우 L-value를 R-value로 변경해줍니다. 이를 통해서 이동과 관련한 연산들이 호출되게 됩니다. (함수 인자가 && 인 것들)
이걸 보면 기존의 일반적인 대입은 R-value를 대상으로 복사를 해서 비효율적인 연산이 필요하게 됩니다. 대신 std::move와 R-value reference를 인자로 받는 생성자, 대입 연산자를 정의하면 복사의 과정 없이 바로 이동이 되어 새롭게 복사를 하는 과정이 없게 됩니다. 얕은 복사라고 생각하면 편하게 이해할 수 있을 것 같네요.
따라서 C++ 에서 볼 수 있는 move의 경우 얕은 복사라고 이해하면 될 듯합니다.
왜 move semantics 인가?
기존의 연산이 부족했을까? 사실 생각해 봤을 때 객체 안에 엄청 큰 배열이 있다고 해보자. 이걸 다른 객체로 옮긴다고 하면 복사를 통해서 엄청난 연산을 잡아먹게 될 것입니다. 대신 객체를 가리키는 메모리 주소를 가리키는 포인터만 슬쩍 바꾼다면 복사에 큰 시간이 들지 않고 옮길 수 있게 됩니다.
위 코드에서 이전 객체 관리자에게 nullptr을 주는 건 이 과정에서 같은 메모리에 권한을 여러 곳에 주면 delete시 오류가 날 가능성이 농후하니 이전 객체에서 소유권을 뺏어가는 것이죠. 이게 move semantice에서 필수라는 이야기는 아닙니다.
swap과 관련한 이야기
이는 이펙티브 C++에서도 중요하게 다루게 되었는데 이번에 move와 함께 공부하면서 swap과 연관해 이해하는 내용이 많이 있었습니다.
template <typename T> void swap(T &a, T &b) { T tmp(a); a = b; b = tmp; }
보통 swap 함수는 '복사 후 맞바꾸기' 전략을 사용해 구현이 되어 있습니다. 그런데 위에 예시처럼 객체가 너무 크기에 복사보다는 move를 통해서 swap 함수를 구현하고 싶은 경우가 있을 것입니다.
일단 기존 std에 있는 swap 함수가 사용될 수 있으니 이펙티브 C++ 항목 25에 나온 내용처럼 using std::swap;을 사용한 뒤 객체 내부에 swap 함수를 구현, 객체와 같은 네임스페이스 안에 비멤버 swap 함수를 만들어 둡니다.
template <typename T> void swap(T &a, T &b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
위 경우 대입 이동 연산자를 구현했기 때문에 위 함수의 move가 동작할 수 있게 됩니다. 이런 식으로 기존 swap 함수보다 빠르게 교체를 할 수 있게 됩니다. 여기서 주의할 점은 std::move는 lvalue를 rvalue로 변경하는 것뿐이지 실제 이동을 하는 함수는 아니다 라는 것입니다. 실제 이동은 우리가 정의한 대입 생성자, 대입 이동 연산자에서 하는 것이죠.
마무리 ver 1.0
일단 잠깐의 마무리를 해보겠습니다.
- move semantics는 얕은 복사처럼 동작. 실제 객체가 바뀌지 않고 객체를 가리키는 포인터의 이동만 있는 것
- std::move의 경우 lvalue를 rvalue로 변경해준다. 그 외 동작은 없음
- std::move로 rvalue 변경에 성공하면 이를 사용하기 위해 객체에 이동 생성자, 이동 대입 연산자가 있어야 합니다.
이 정도로 정리를 했는데 더 찾아보면 복잡한 내용의 이야기들이 많습니다. 그래서 이후의 부분들이 머릿속에 정리가 된다면 또다시 글을 써 보도록 하겠습니다. 당장 이후의 자료가 급하신 분들은 아래 참고 자료를 확인하시면 원하는 내용을 얻을 거란 예상이 됩니다.
참고자료
반응형'쾌락없는 책임 (공부) > C++ 짜잘이' 카테고리의 다른 글
[C++] C++ iterator, 반복자 - 1 (0) 2022.08.13 [C++] shared_ptr 은 어떻게 동작하게 될까? (0) 2022.08.09 [C++] C++ 로 중복 없는 랜덤 변수 만드는 방법 - 1 (0) 2022.08.05 [C++] C++ 의 '1LL'은 무슨 뜻일까? (0) 2022.06.16 C++ make_pair vs {} (0) 2022.03.06