[C++] 로컬 반환에서 std::move가 RVO/NRVO를 막는 이유


https://en.cppreference.com/w/cpp/language/copy_elision

RVO/NRVO란

  • RVO (Return Value Optimization): 함수가 임시 객체(prvalue)를 직접 반환할 때, 반환용 임시와 호출자 쪽 객체를 하나로 합쳐서 생성하는 최적화. 복사/이동 생성자가 아예 호출되지 않는다.
  • NRVO (Named RVO): 함수 내부의 이름 있는 로컬 변수를 return var;로 반환하는 경우에도, 구현이 가능하면 그 로컬을 호출자 대상 위치에 직접 생성한다. 이것도 복사/이동 호출이 없을 수 있다.
  • C++17 이후: return T{...}; 처럼 직접 prvalue를 반환하는 패턴은 보장된(copy elision) 생략이라서, 복사/이동 호출이 규칙적으로 없다. 반면 NRVO는 여전히 선택적 최적화다(대부분의 최적화 빌드에서 일어난다).

요지는 간단하다. 컴파일러가 복사/이동 자체를 없애줄 수 있으니 괜히 우리가 std::move로 개입해 이 최적화 기회를 망치지 말자는 것.

먼저 코드부터 보자.

// 예시 1: NRVO 대상
std::string make_msg() {
    std::string s = "hello";
    return s;             // 많은 구현에서 NRVO 적용 → 실제 복사/이동 없음
}

// 예시 2: NRVO를 스스로 차단
std::string make_msg_move() {
    std::string s = "hello";
    return std::move(s);  // s를 '이동해야 하는 값'으로 고정 → 이동 생성자 호출될 수 있음
}

핵심은 다음과 같다.

  1. return s; 는 NRVO 후보라서, 구현이 최적화하면 복사/이동 자체가 사라진다.
  2. return std::move(s); 는 NRVO 기회를 줄이고 실제 이동을 강제한다. 이동도 비용이 0이 아니다.
  3. C++17부터 return T{...}; 같은 직접 prvalue 반환은 “보장된 복사 생략” 규칙으로 아예 복사/이동 호출이 없다.
// C++17: 보장된 복사 생략
std::string make_direct() {
    return std::string{"hello"};   // 복사/이동 호출 없음
}

returnstd::move를 붙이는 이유

  1. “이동이 복사보다 빠를 것이다"라는 일반론:
    이동이 보통 더 싸긴 하지만, 최적화로 복사/이동 자체가 사라지는 경우엔 std::move를 붙여 오히려 이동을 강제하게 된다.

  2. 값 범주(Value Category)에 대한 오해:
    로컬 변수 var는 lvalue다. return var;는 NRVO 후보고, return std::move(var);는 xvalue(이동 대상)로 바뀐다. 많은 이들이 “lvalue면 비싸니 무조건 move”로 오해한다.

  3. 매개변수 반환과 로컬 반환을 혼동:
    매개변수 T x를 그대로 반환할 땐 return std::move(x);가 의미가 있을 수 있다(NRVO 대상이 아니므로). 이 패턴을 로컬 반환에도 그대로 이식하면서 문제가 생긴다.

  4. 컴파일러가 알아서 다 해줄 거라는 막연한 믿음:
    NRVO는 선택적 최적화다. 대부분 해주긴 하지만, 우리가 std::move를 붙이면 최적화를 방해할 수 있다.

해결 방법

  1. 로컬을 반환할 땐 굳이 std::move를 붙이지 않는다.
T f() {
    T x = /* ... */;
    return x;          // NRVO 기대 (복사/이동 호출 자체가 사라질 수 있음)
}
  1. 가능하면 직접 prvalue를 만들어 반환한다. (C++17)
T f() {
    return T{/* ... */};  // 보장된 복사 생략
}
  1. 매개변수나 멤버를 그대로 반환할 때만 std::move를 고려한다. (매개변수는 함수 안에서 lvalue이므로 return x; 는 복사다.)
std::string id(std::string x) {
    return std::move(x);   // 여기서는 이동이 의미가 있다
}

template<class T>
T pass_through(T&& x) {
    return std::forward<T>(x); // 전달 참조일 땐 forward
}
  1. 조건 분기로 서로 다른 로컬을 반환하며 NRVO가 깨질 것 같다면, 각 분기에서 직접 prvalue를 만들어 반환한다.
std::string g(bool b) {
    if (b) return std::string{"A"};
    else   return std::string{"B"};  // C++17 보장된 복사 생략
}

comments