ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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++)

    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

    반응형

    댓글

Designed by Tistory.