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
Similar Posts: