[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를 '이동해야 하는 값'으로 고정 → 이동 생성자 호출될 수 있음
}
핵심은 다음과 같다.
return s;
는 NRVO 후보라서, 구현이 최적화하면 복사/이동 자체가 사라진다.return std::move(s);
는 NRVO 기회를 줄이고 실제 이동을 강제한다. 이동도 비용이 0이 아니다.- C++17부터
return T{...};
같은 직접 prvalue 반환은 “보장된 복사 생략” 규칙으로 아예 복사/이동 호출이 없다.
// C++17: 보장된 복사 생략
std::string make_direct() {
return std::string{"hello"}; // 복사/이동 호출 없음
}
return
에 std::move
를 붙이는 이유
-
“이동이 복사보다 빠를 것이다"라는 일반론:
이동이 보통 더 싸긴 하지만, 최적화로 복사/이동 자체가 사라지는 경우엔std::move
를 붙여 오히려 이동을 강제하게 된다. -
값 범주(Value Category)에 대한 오해:
로컬 변수var
는 lvalue다.return var;
는 NRVO 후보고,return std::move(var);
는 xvalue(이동 대상)로 바뀐다. 많은 이들이 “lvalue면 비싸니 무조건 move”로 오해한다. -
매개변수 반환과 로컬 반환을 혼동:
매개변수T x
를 그대로 반환할 땐return std::move(x);
가 의미가 있을 수 있다(NRVO 대상이 아니므로). 이 패턴을 로컬 반환에도 그대로 이식하면서 문제가 생긴다. -
컴파일러가 알아서 다 해줄 거라는 막연한 믿음:
NRVO는 선택적 최적화다. 대부분 해주긴 하지만, 우리가std::move
를 붙이면 최적화를 방해할 수 있다.
해결 방법
- 로컬을 반환할 땐 굳이
std::move
를 붙이지 않는다.
T f() {
T x = /* ... */;
return x; // NRVO 기대 (복사/이동 호출 자체가 사라질 수 있음)
}
- 가능하면 직접 prvalue를 만들어 반환한다. (C++17)
T f() {
return T{/* ... */}; // 보장된 복사 생략
}
- 매개변수나 멤버를 그대로 반환할 때만
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
}
- 조건 분기로 서로 다른 로컬을 반환하며 NRVO가 깨질 것 같다면, 각 분기에서 직접 prvalue를 만들어 반환한다.
std::string g(bool b) {
if (b) return std::string{"A"};
else return std::string{"B"}; // C++17 보장된 복사 생략
}