ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C++] 이동 의미론 Move Semantics
    프로그래밍 언어 2023. 11. 20. 23:20

    C++11부터 복제를 통한 자원의 낭비를 줄이기 위해 객체 자원의 소유권을 이전할 수 있도록 추가된 개념이다.

     

    Foreword

    이전에는 복제를 통해 자원의 소유권을 이전했었다. 복제 방식은 2가지로 얕은 복제(Shallow Copy)와 깊은 복제(Deep Copy)가 있다.

     

    얕은 복제란 원본 객체의 데이터 맴버를 대상 객체로 단순히 복제하거나 대입하는 방식으로 참조는 복제되지만 참조되는 객체는 복제되지 않는다.

    얕은 복제 (Shallow Copy)

     

    얕은 복제 사용 시 객체의 할당 메모리를 해제할 경우 허상 포인터나 미아(Orphan)으로 인한 메모리 누수의 문제점이 존재한다.

     

    이런 문제점을 해결하기 위해 깊은 복제를 사용하는데 깊은 복제란 참조값의 복제가 아닌 참조된 객체 자체가 복제되는 것을 뜻한다.

    깊은 복제 (Deep Copy)

     

    단, 깊은 복제를 사용하기 위해선, 클래스에 동적 할당 메모리가 있다면 복제 생성자와 복제 대입 연산자를 직접 정의해야 하며, 복제를 위한 추가적인 메모리 비용이 발생한다.

    // The Simple Example of Copy Constructor and Copy Assignment Operator
    class SpreadsheetCell
    {
    public:
        SpreadsheetCell(const SpreadsheetCell& src)   // copy constructor
            : mValue(src.mValue)
        {
    	
        }
    
        SpreadsheetCell& operator=(const SpreadsheetCell& rhs)   // copy-assignment operator
        {
            if(this == &rhs) return *this;
            mValue = rhs.mValue;
            return *this;
        }
    
    private:
        double mValue = 0;
    }

     

    Move Semenatic

    이를 해결하기 위해 이동 의미론이라는 개념이 추가되었으며, 원본 객체를 삭제할 때 유용하게 사용할 수 있다.

     

    이동 의미론을 이해하기 위해선 좌측값(L-Value), 우측값(R-Value) 두 가지 개념에 대해 알아볼 필요가 있다.

     

    좌측값이란 쉽게 말해 할당 연산자의 왼쪽에 위치 가능한 값이다. 변수처럼 이름과 주소를 가진 대상을 뜻하며 좌측값 레퍼런스는 일반적인 레퍼런스와 동일하다.

     

    우측값이란 할당 연산자의 오른쪽에 위치 가능한 값으로 리터럴, 임시 객체, 값처럼 좌측값이 아닌 모든 대상을 뜻한다. 우측값 레퍼런스는 &&로 표현하며 임시 객체에 대해 적용할 함수를 컴파일러가 선택하기 위한 용도이다.

     

    이동 의미론을 구현하기 위해선 이동 생성자와 이동 대입 연산자를 구현해야 한다. 또한 표준 라이브러리와 호완성을 유지하기 위해 이동 생성자와 이동 대입 연산자를 noexcept으로 지정해야 한다.

    이동 의미론 Move Semantic

    class Spreadsheet
    {
    public:
        Spreadsheet(Spreadsheet&& src) noexcept   // move constructor
        {
            swap(*this, src);
        }
    
        Spreadsheet& operator=(Spreadsheet&& src) noexcept   // move-assignment operator
        {
            Spreadsheet temp(std::move(rhs);
            swap(*this. temp);
            return *this;
        }
    
    private:
        size_t mWidth = 0;
        size_t mHeight = 0;
        SpreadsheetCell** mCells = nullptr;
    }
    
    void swap(Spreadsheet& first, Spreadsheet& second) noexcept
    {
        using std::swap;
        swap(first.mWidth, second.mWidth);
        swap(first.mHeight, second.mHeight);
        swap(first.mCells, second.mCells);
    }

     

    여기서 C++에서 지켜야 하는 규칙 세 가지가 나온다.

    1. 3의 규칙 Rule of Three : 소멸자, 이동 생성자, 이동 복사 연산자 중 하나라도 필요하면 모두 정의해야 한다.
    2. 5의 규칙 Rule of Five : 클래스에 동적 할당 메모리를 사용하는 코드를 작성했다면, 소멸자, 이동 생성자, 복사 생성자, 이동 대입 연산자, 복사 대입 연산자 다섯 가지 특수 맴버 함수들을 구현해야 한다.
    3. 0의 규칙 Rule of Zero : 소멸자, 이동 생성자, 복사 생성자, 이동 대입 연산자, 복사 대입 연산자 다섯 가지 특수 맴버 함수들을 필요로 하지 않도록 디자인 해야한다. 메모리를 동적 할당하지 않고 표준 라이브러리 컨테이너와 같은 최신 구문을 활용한다.

     

    std::move

    이러한 이동 의미론을 활용하기 위해 <utility> 내 move라는 템플릿 함수가 정의되어 있다.

    template<class T>
    constexpr std::remove_reference_t<T>&& move(T&& t) noexcept;
    {
        return static_cast<typename std::remove_reference<T>::type&&>(t);
    }

     

    해당 함수의 반환값은 std::remove_reference_t<T>&&로 T 타입의 xvalue의 레퍼런스를 제거하고 우측값 레퍼런스를 추가한 값을 반환한다. 해당 값을 좌측값에 대입하여 복제를 사용하지 않고도 자원의 소유권을 이전할 수 있다.

    '프로그래밍 언어' 카테고리의 다른 글

    OOP Design Pattern - Structural Pattern  (0) 2024.01.18
    OOP Design Pattern - Creational Pattern  (0) 2024.01.12
    입실론 테스트  (0) 2023.11.20
    객체 지향 설계 SOLID  (0) 2023.11.16
    Zero Division with Floating-Point Numbers  (0) 2023.11.16
Designed by Tistory.