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의 유용성을 한 번 느껴보시는 것도 좋을 것 같습니다.