쾌락없는 책임 (공부)/C++ 짜잘이

[C++] C++의 move semantics (의미론적 이동?)

허스크 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++)

docs.microsoft.com

 

씹어먹는 C++ - <12 - 2. Move 문법 (std::move semantics) 과 완벽한 전달 (perfect forwarding)>

 

modoocode.com

 

 

What is move semantics?

I just finished listening to the Software Engineering radio podcast interview with Scott Meyers regarding C++0x. Most of the new features made sense to me, and I am actually excited about C++0x now...

stackoverflow.com

반응형