본문 바로가기

Programming/C++

[C++11] Rvalue Reference #2 - Move Semantics

Move Semantics

 

Move Semantics란 객체의 리소스(동적으로 할당 된 메모리와 같은)를 또 다른 객체로 전송(이동)하는 것을 의미합니다. 앞에서 살펴보았던 Rvalue 참조자는 Move Semantics의 구현을 가능하게 하고 이로 인해 상당한 성능을 향상시킬 수 있습니다. Rvalue는 프로그램 어디에서도 참조될 수 없는 임시 객체이지만 Rvalue 참조자를 이용하여 임시 객체의 리소스를 이동시킴으로서 쓸데없는 메모리 할당과 복사 작업을 생략하여 성능이 향상되는 것이죠. 

 

복잡한 설명보다는 간단한 예제를 보면서 설명을 드리는게 좋을 것 같네요. 메모리 버퍼를 관리하는 MemoryBlock 이라는 클래스를 작성하고 이 클래스 객체를 vector에 삽입하는 코드를 작성해보도록 하겠습니다. 

 

// MemoryBlock.h
#pragma once
#include <iostream>
#include <algorithm>

class MemoryBlock
{
public:

    // 버퍼 크기를 넘겨받는 생성자
    explicit MemoryBlock(size_t length)
        : _length(length)
        , _data(new int[length])
    {
        std::cout << "In MemoryBlock(size_t). length = "
            << _length << "." << std::endl;
    }

    // 소멸자
    ~MemoryBlock()
    {
        std::cout << "In ~MemoryBlock(). length = "
            << _length << ".";

        if (_data != NULL)
        {
            std::cout << " Deleting resource.";
            // 리소스 삭제
            delete[] _data;
        }

        std::cout << std::endl;
    }

    // 복사 생성자
    MemoryBlock(const MemoryBlock& other)
        : _length(other._length)
        , _data(new int[other._length])
    {
        std::cout << "In MemoryBlock(const MemoryBlock&). length = "
            << other._length << ". Copying resource." << std::endl;

        std::copy(other._data, other._data + _length, _data);
    }

    // 대입 연산자
    MemoryBlock& operator=(const MemoryBlock& other)
    {
        std::cout << "In operator=(const MemoryBlock&). length = "
            << other._length << ". Copying resource." << std::endl;

        if (this != &other)
        {
            // 기존 리소스 삭제
            delete[] _data;

            _length = other._length;
            _data = new int[_length];
            std::copy(other._data, other._data + _length, _data);
        }
        return *this;
    }

    // 리소스의 길이를 반환
    size_t Length() const
    {
        return _length;
    }

private:
    size_t _length;  // 리소스 길이
    int* _data;        // 리소스
};

 

#include "MemoryBlock.h"
#include <vector>

using namespace std;

int main()
{
    // MemoryBlock에 대한 vector를 생성하여 두 개의 원소를 추가
    vector<MemoryBlock> v;
    v.push_back(MemoryBlock(25));
    v.push_back(MemoryBlock(75));

    // 첫 번째 원소를 다른 MemoryBlock 으로 변경
    v[0] = MemoryBlock(50);
}

 

 

위의 예제에서 구현한 MemoryBlock 클래스는 지금까지 많이 보아왔던 형태의 클래스입니다. 복사 생성자와 대입 연산자를 구현하여 다른 객체로부터의 객체 생성과 복사가 가능하죠. main 함수에서 임시 객체를 이용하여 vector에 원소를 삽입할 때 복사 생성자가 이용되고 삽입된 원소를 또다른 임시 객체로 변경할 때 대입 연산자가 이용됩니다. 동작은 올바르게 구현이 되었지만 불필요한 메모리 할당과 복사 작업이 발생하여 효율성이 떨어지는 코드가 되고 말았습니다. v[0] = MemoryBlock(50); 코드가 어떻게 동작되는지 다음 그림을 통해 살펴보도록 합시다.

 

    
    
<그림 1: 데이터의 복사>
 
 
임시 객체가 복사되는데에 ①~⑤의 과정을 모두 거쳐야 한다는 것은 성능과 효율성 측면에서 큰 낭비가 아닐 수 없습니다. 임시 객체는 표현식이 종료되면 어짜피 사라지게될 객체이므로 아래 그림과 같이 기존 데이터를 삭제하고 임시 객체의 데이터를 가리키도록 구현할 수 있다면 훨씬 더 효율적이지 않을까요?

    
<그림 2: 데이터의 이동>
         
 

Move 생성자와 Move 대입 연산자

 

Rvalue로 부터의 데이터 이동, 즉 Move Semantics의 가장 일반적인 구현 방법은 바로 Move 생성자와 Move 대입 연산자의 구현입니다. Move 생성자와 Move 대입 연산자는 기존의 복사 생성자, 대입 연산자의 형태와 매우 흡사하며 파라미터 타입을 Rvalue 참조자로 변경함으로서 구현이 가능합니다. 아래 예제에서는 Move 생성자와 Move 대입 연산자를 추가하여 <그림 2: 데이터의 이동>을 구현하는 방법을 보여줍니다. 

 
// Move 생성자
MemoryBlock(MemoryBlock&& other)
    : _data(NULL)
    , _length(0)
{
    std::cout << "In MemoryBlock(MemoryBlock&&). length = "
        << other._length << ". Moving resource." << std::endl;

    // 원본 객체로부터 리소스 포인터와 길이를 복사
    _data = other._data;
    _length = other._length;

    // 원본 객체 소멸시 소멸자에서 리소스를 삭제하지 않도록 초기화
    other._data = NULL;
    other._length = 0;
}

// Move 대입 연산자
MemoryBlock& operator=(MemoryBlock&& other)
{
    std::cout << "In operator=(MemoryBlock&&). length = "
        << other._length << "." << std::endl;

    if (this != &other)
    {
        // 기존 리소스 삭제
        delete[] _data;

        // 원본 객체로부터 리소스 포인터와 길이를 복사
        _data = other._data;
        _length = other._length;

        // 원본 객체 소멸시 소멸자에서 리소스를 삭제하지 않도록 초기화
        other._data = NULL;
        other._length = 0;
    }
    return *this;
}

 

 

강좌 시작 부분에서 Move Semantics에 대한 개념을 풀어서 설명할 때는 어렵게 느껴졌지만 실제 구현된 코드를 보니 참 간단합니다. 이로서 메모리의 할당과 데이터의 복사 작업이 최소화되어 성능이 대폭 향상되었습니다. 위 예제 처럼 임시 객체의 직접적인 대입 이외에도 vector와 같은 컨테이너에 담는 작업이 빈번한 클래스나 STL의 sort와 같은 함수로 정렬하는 작업이 많은 클래스는 Move 생성자와 Move 대입연산자를 구현하면 성능 향상에 많은 도움이 됩니다.

 

Move 생성자와 Move 대입 연산자 이외에도 Rvalue 참조자로 일반 함수를 오버로드 하면 Move semantics를 구현할 수 있습니다.  Visual Studio 2010 이상 버전에서는 std::string의 operator+ 함수를 비롯한 STL의 많은 일반 함수들이 Move Semantics를 구현하여 성능 향상을 도모하고 있습니다.

 

지금까지 Move Semantics의 개념과 Rvalue 참조자를 Move Semantics 구현에 활용하는 방법에 대하여 살펴보았습니다. 어찌보면 간단한 내용인데 이해하기 쉽게 설명하려다보니 내용이 조금 길어졌네요. 부디 이해가 잘 되셨기를 바라면서 이번 강좌를 마치겠습니다. ^^

 

 

Reference

 

MSDN - Rvalue Reference Declarator: &&

MSDN - How to: Write a Move Constructor

Move Semantics and Perfect Forwarding in C++11

 

 

원문 링크

 

DevMachine's Blog


출처

http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNO=20&no=8626&page=1

'Programming > C++' 카테고리의 다른 글

Lvalue Rvalue  (1) 2013.01.21
에러 핸들링(Error Handling)  (0) 2012.10.24
this pointer  (0) 2012.09.16
error C2065: 'IDD_Main_DIALOG' : 선언되지 않은 식별자입니다.  (0) 2012.07.25
Bitmap File구조  (0) 2009.09.19