C++에서 const Klass&반환값 형태의 단점들을 쓴지도 시간이 많이 지났네요. C++11에서는 많은 것이 바뀌었습니다. 대표적인 것이 rvalue reference로 대표되는 Move semantics입니다. Move는 RVO(return value optimization)가 동작할 수 없을 때 객체의 복사비용을 줄이는 목적으로 사용됩니다. Move는 객체를 “복사”하는 대신 객체가 내부에 가진 포인터만 가져옵니다. 그런이유로 속도가 매우 빠릅니다.
이 글에서는 rvalue reference와 관련해 함수의 리턴 타입과 적절한 반환값에 대해서 살펴보겠습니다.
지역 변수를 반환할 때
Herb Sutter는 widget* load_widget() 처럼 포인터를 반환하는 구시대적(?) 리턴 타입을 비판하면서 현대적 방식으로 다음 두가지 규칙을 제안했습니다.
1. 만약 반환할 객체에 다형성이 필요하다면 unique_ptr로 반환한다.
2. 만약 다형성이 필요없다면 그 반환할 객체가 값임을 의미한다. 그러므로 복사나 이동이 가능한 값으로 반환한다.
1번 규칙은 Scott Meyer의 Modern Effective C++에서 factory가 unique_ptr을 반환해야한다고 제안한 것과 일맥 상통합니다. 또 누가봐도 메모리 관리가 편하고, 메모리의 소유권이 명확하며, 객체의 복사가 필요없는 unique_ptr을 반환하는건 명확해 보입니다.
2번이 문제인데, 대체 어떻게 값으로 반환해야하는가 역시 고민거리이기 때문입니다. 이에 대한 해답을 다음 코드로 제시합니다.
#include <iostream> #include <memory> using namespace std; class Foo { public: Foo(): destructed_(false) { cout << "ctor" << endl; } Foo(const Foo& other) { cout << "copy ctor" << endl; } Foo(Foo&& other) { cout << "move ctor" << endl; } Foo& operator=(const Foo& rhs) { cout << "copy assign"; } Foo& operator=(Foo&& rhs) { cout << "move assign"; } ~Foo() { destructed_ = true; } bool destructed_; }; Foo retVal() { Foo f; // 여기서 만들어진 객체가 그대로 main에서 사용된다. return f; // RVO가 동작하므로 복사도 이동도 불필요 } Foo retMove() { Foo f; return move(f); // move를 명시하므로 move가 우선해서 사용됨 } Foo&& retDangling() { Foo f; // reference 반환시 객체가 파괴되므로 런타임 오류. // reference는 항상 살아있는 객체에 대해서만 반환 해야한다. return move(f); } Foo retParam(Foo param_f, bool b) { // if-else로 인해 RVO가 동작하지 않는 경우. if (b) { Foo local_f; // 만약 RVO가 동작하지 않으면 자동으로 move가 시도된다. // 사실 이 경우가 move가 등장한 배경 중 하나. return local_f; // move! } return param_f; // move! } int main() { cout << "retVal" << endl; Foo f = retVal(); cout << endl << "retMove" << endl; Foo f2 = retMove(); cout << endl << "retParam, true" << endl; Foo f3 = retParam(Foo(), true); cout << endl << "retParam, false" << endl; Foo f4 = retParam(Foo(), false); cout << endl << "retDangling" << endl; Foo&& f5 = retDangling(); cout << "f5 is " << (f5.destructed_ ? "destructed" : "live") << endl; return 0; }
출력은 다음과 같습니다.
retVal ctor retMove ctor move ctor retParam, true ctor ctor move ctor retParam, false ctor move ctor retDangling ctor f5 is destructed
위 코드로 미루어볼 때 아무것도 모르는 사람이 코딩하듯이 지역변수를 반환할 때는 move없이 값으로 반환하는 것이 최상임을 알 수 있습니다. 그러면 RVO가 가능하면 RVO가 되고, 안되면 move가 시도되는 것을 알 수 있습니다. 그도 안되면 copy가 되겠죠.
이 규칙에는 한가지 예외가 있습니다. 반환하는 지역변수의 타입과 함수의 리턴 타입이 일치하지 않는 경우입니다. 다음은 함수의 리턴 타입은 optional<Foo>인데 실제 반환하는 값은 Foo인 경우를 보여줍니다. 이 경우에는 RVO나 move가 자동으로 동작하지 않아 move()를 반드시 해줘야합니다. 그러나 애초에 이런 암시적인 형변환에 의한 반환 자체가 나쁜거겠죠. 처음부터 optional<Foo>를 반환하면 될일입니다.
optional<Foo> returnOptional() { Foo f; return move(f); }
객체의 멤버를 반환할 때
객체의 멤버 변수를 반환할 때는 지역변수와 달리 반드시 move를 해야합니다. 이에 대한 예를 다음 코드에 보였습니다.
#include <algorithm> #include <iostream> #include <utility> #include <vector> using namespace std; class Foo { public: Foo(int v): value_(v) { cout << "foo ctor" << endl; } Foo(const Foo& other): value_(other.value_) { cout << "foo copy" << endl; } Foo(Foo&& other): value_(other.value_) { cout << "foo move" << endl; } int value() const { return value_; } ~Foo() { destructed_ = true; } bool isDead() { return destructed_; } private: int value_; bool destructed_ = false; }; class C { public: C() { vals_.push_back(1); cout << "C ctor" << endl; } C(const C& other) { cout << "C copy" << endl; } C(C&& other) { cout << "C move" << endl; } ~C() { } void add(int v) { vals_.push_back(v); } vector<Foo> ret_val() &&; vector<Foo> ret_move() &&; private: vector<Foo> vals_; }; // 함수명 뒤의 &&는 rvalue reference에 호출되는 함수임을 의미 vector<Foo> C::ret_val() && { // 그냥 반환하면 copy. // 지역변수의 경우와 달리 rvalue임에도 RVO나 move가 자동으로 되지 않는다. return vals_; } vector<Foo> C::ret_move() && { // move가 수행됨 return move(vals_); } int main() { cout << "vals_ret" << endl; auto vals_ret_val = C().ret_val(); cout << endl << "vals_move" << endl; auto vals_ret_move = C().ret_move(); return 0; }
다음은 실행 결과입니다.
vals_ret foo ctor foo move C ctor foo copy vals_move foo ctor foo move C ctor