Rvalue reference와 함수의 반환값

Tags:

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