C++11의 rvalue reference 흉내내기

Posted at 2012. 7. 1. 22:40 // in S/W개발/C++ 이야기 // by 김윤수


C++11의 큰 특징 중의 하나로 rvalue reference라는 것이 도입되었습니다. reference라는 것이 기본적으로 lvalue로 작동할 수 있는 것인데 rvalue reference라는 개념이라는 게 있을 수가 있나 하는 생각이 들지만, 불필요한 임시객체 생성 및 삭제로 인한 성능 저하를 막기 위해 도입된 것이라 할 수 있습니다. 일명 move semantic과 perfect forwarding을 위한 것이라고 합니다.


성능이라는 단어가 나오면 보통 C++ 프로그래머들은 관심을 갖기 마련이고, 그런 좋은 것이 있다면 빨리 쓰고 싶기도 할 터인데요, C++11을 일부 지원하는 기능을 컴파일러가 시장에 나오긴 했더라도 자신이 속한 프로젝트에서는 정책상 C++11 기능을 사용하지 못하게 해 놓을 수도 있을 것입니다. 그렇다고 이 좋은 기능을 그냥 보고 넘어갈 수도 없는 노릇이겠지요.


그래서 한 번 생각해 봤습니다. rvalue reference를 흉내낼 수는 없을까하구요. 한 동안 고민하고 있었는데, 어느 순간 갑자기 아이디어가 떠오르더군요. 떠오른 생각을 빠르게 한 번 옮겨 봤습니다.


#include <iostream>


using namespace std;


namespace buffer {


// 테스트용 클래스

class Foo {

public:

    Foo() {

        cout << __PRETTY_FUNCTION__ << endl;

    }

    ~Foo() {

        cout << __PRETTY_FUNCTION__ << endl;

    }

};


class BufferRvRef;


class Buffer {

public:

    // 아래 template Move 함수의 일반적 정의를 위한 보조 타입

    typedef BufferRvRef RvRef;

    Buffer() : buf_(new Foo()), id_(id) {

        cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;

        ++id;

    }

    // move constructor

    // rvalue reference를 받는 경우를 흉내내서 move semantic 

    // 구현

    Buffer(const RvRef& rvref);

    ~Buffer() {

        cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;

        delete buf_;

    }


private:

    // move semantic 구현. copy construction시 const 객체를 

    // 받으므로 const 멤버 함수로 정의함

    Foo* release() const {

        cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;

        Foo* ret = buf_;

        buf_ = 0;

        return ret;

    }

    // release() 함수에서 buf_ 조작을 위해서 mutable 붙임

    mutable Foo* buf_;

    // 화면 출력 메시지에서 각 객체 구분 용도. 실제 클래스라면 불필요

    int id_;

    // BufferRvRef가 release()를 호출할 수 있도록 하기 위해 

    // friend로 선언

    friend class BufferRvRef;

    static int id;

};


int Buffer::id = 0;


// Buffer 내부 데이터를 move 시키기 위한 임시 저장소 역할

class BufferRvRef {

public:

    // const Buffer 객체를 임시로 저장함

    explicit BufferRvRef(const Buffer& buf) 

        : buf_(buf.release()), id_(id) {

        cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;

        ++id;

    }

    BufferRvRef(const BufferRvRef& other) 

        : buf_(other.release()), id_(id) {

        cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;

        ++id;

    }

    // move semantic 구현

    Foo* release() const {

        cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;

        Foo* ret = buf_;

        buf_ = 0;

        return ret;

    }

    ~BufferRvRef() {

        cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;

        delete buf_;

    }


private:

    // release() 함수에서 buf_ 조작을 위해 mutable 붙임

    mutable Foo* buf_;

    // 화면 출력 메시지에서 각 객체 구분 용도. 실제 클래스라면 불필요

    int id_;

    static int id;

};


int BufferRvRef::id = 0;

inline Buffer::Buffer(const Buffer::RvRef& rvref
    : buf_(rvref.release()), id_(id) {
    cout << __PRETTY_FUNCTION__ << " - " << id_ << endl;
    ++id;
}

}

// Move() 템플릿 함수가 하는 역할은 T를 T::RvRef로 형변환해주는 역할
// Buffer 라면 Buffer::RvRef(BufferRvRef형)로 형변환해주므로
// BufferRvRef(const Buffer&buf)가 호출되면서 Buffer.buf_가
// BufferRvRef.buf_로 옮겨감
template <typename T>
typename T::RvRef Move(const T& buf) {
    cout << __PRETTY_FUNCTION__ << " starts" << endl;
    return typename T::RvRef(buf);
}

// Buffer를 value로 반환하게 되는 함수는 이전과 다르게
// Move()를 호출할 경우, move semantic을 구현하는
// constructor가 호출됨
buffer::Buffer ReturnBuffer() {
    cout << __PRETTY_FUNCTION__ << " starts" << endl;
    return Move(buffer::Buffer());
}

int main() {
    cout << __PRETTY_FUNCTION__ << " starts" << endl;
    buffer::Buffer buf(ReturnBuffer());
    cout << __PRETTY_FUNCTION__ << " ends" << endl;
}

위와 비슷한 형태로 작성하면 move semantic을 구현할 수 있을 것 같습니다. 중요한 것은 rvalue reference를 흉내낼수 있는 클래스(FooRvRef)를 각 클래스별(Foo)로 하나 정의하고, Foo --> FooRvRef 변환, FooRvRef --> Foo 변환이 가능하게 하고, 이 변환시 resource를 가져오는 방식으로 구현하면 될 것 같습니다. 물론 value를 반환하는 경우에 일관성있게 Move() 함수를 호출해 줘야만 원하는 변환이 일어날 수 있을 것입니다(ReturnBuffer() 참조). 마지막으로 Move()라는 함수 구현을 쉽게 할 수 있도록 move enable 시키고 싶은 클래스에는 일관성 있게 Foo::RvRef 라는 typedef 를 정의해 줘야합니다. 

다음은 위 프로그램을 한 번 실행시켜본 결과입니다.

$ ./rvalemul 
int main() starts
buffer::Buffer::RvRef ReturnBuffer() starts
buffer::Foo::Foo()
buffer::Buffer::Buffer() - 0 constructed
typename T::RvRef Move(const T&) [with T = buffer::Buffer, typename T::RvRef = buffer::BufferRvRef] starts
buffer::Foo* buffer::Buffer::release() const - 0
buffer::BufferRvRef::BufferRvRef(const buffer::Buffer&) - 0 move constructed
buffer::Buffer::~Buffer() - 0 destroyed
buffer::Foo* buffer::BufferRvRef::release() const - 0
buffer::Buffer::Buffer(const RvRef&) - 1 move constructed
buffer::BufferRvRef::~BufferRvRef() - 0 destroyed
int main() ends
buffer::Buffer::~Buffer() - 1 destroyed
buffer::Foo::~Foo()

위 출력 메시지를 자세히 쫒아가시면 BufferRvRef가 중간에 개입되어 main() 함수에 선언된 buf 객체가 move construction 되고 있는 것을 확인할 수 있습니다.

추가적으로 알아둘만한 점은 move semantic은 성능을 위해서도 좋지만, 예외 처리를 위해서도 좋다는 점입니다. 왜냐하면 copy construction이 deep copy가 일어난다면 통상 일시적으로 두 개의 copy가 생성되는 셈인데, 그 와중에 exception이 발생할 가능성이 아무래도 커지고, 메모리가 부족할 가능성이 있을 것이니 아무래도 value 반환을 위해서는 move semantic이 훨씬 안전한 방식이라고 생각이 됩니다.

마지막으로 한 가지 첨언하면 위 Buffer 클래스는 copy constructor를 정의하지 않았으므로 move-only 클래스라고 할 수 있습니다.

혹시 C++11 컴파일러를 아직 사용하지 못하시더라도 위와 같은 방식으로 rvalue reference의 유용성을 한 번 느껴보시는 것도 좋을 것 같습니다.