오른값 참조를 다른 함수에 넘겨줄때는 std::move를 사용하고 보편 참조를 다른 함수에 넘겨줄때는 std::forward를 사용해야 한다.
오른값 참조에 std::forward를 사용하는 것은 동작 자체는 문제가 없지만 실수의 여지가 있다. 보편 참조에 std::move를 사용하는것은 아예 좋지 않은 결과가 초래될 수 있다.
class Widget {
public:
template<typename T>
void setName(T&& newName) { name = std::move(newName); } // 보편 참조를 move로 이동
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
std::string getWidgetName(); // 팩토리 함수
Widget w;
auto n = getWidgetName();
w.setName(n); // 호출이 끝나면 n의 값은 알 수 없어진다
const string&을 받는 함수를 오버로딩 하면 되는거 아닌가? 싶겠지만 리터럴 문자열을 인수로 넘겨줬을때 기존에는 발생하지 않았던 비용이 발생하는 것을 생각하면 그다지 올바르지 않다.
그보다는 코드의 확장이 나쁘다는것이 더 큰 문제이다. 지금은 매개변수가 고작 하나라서 함수 오버로딩이 두개면 됐지만 여러개이고 심지어 왼값, 오른값 모두 존재한다면 오버로딩의 수가 기하급수적으로 늘어난다.
이런 문제를 피하기 위해서 보편 참조에 대해서는 대체로 std::forward를 사용해야 한다.
'항상'이 아니라 '대체로' 라고 했다. 그 이유는 오른값 참조나 보편 참조에 묶인 객체는 함수 내에서 여러번 사용될수도 있기 때문에 전부 다 사용하기 전에 이동되는 일은 피해야 하기 때문이다. 그래서 std::move나 std::forward를 적용하는 것은 해당 참조의 마지막 사용이어야 한다.
또한 std::move의 경우 경우에 따라 std::move_if_noexcept를 사용하는 게 바람직한 경우도 존재한다. (항목 14 참조)
만약 함수가 결과를 값으로 돌려주고 해당 값이 오른값 참조나 보편 참조에 묶인 객체라면 std::move 또는 std::forward를 적용시켜서 반환하는 것이 바람직하다.
그런데 함수 내에서 선언된 지역 변수를 반환하는 경우에도 std::move를 적용하면 복사 연산 대신 이동 연산이 이루어 질거라는 확대 해석을 해서는 안된다.
Widget makeWidget() {
Widget w;
...
return std::move(w);
}
이런 경우는 굳이 std::move를 사용하지 않고 return w만 하더라도 반환값 최적화(RVO)에 의해 복사가 제거되어 복사가 이뤄지지 않는다.
다만 RVO가 적용되려면 지역 객체의 타입이 함수의 반환 타입과 같아야 하고 그 지역 객체가 함수의 반환값이어 한다.
위의 경우는 둘 다 해당되기 때문에 RVO가 적용된다.
굳이 std::move를 적용해서 반환하면 타입이 달라져서 RVO의 조건이 충족되지 않기 때문에 오히려 최적화 여지를 제한하게 되는 결과를 낳게된다.
RVO가 적용되더라도 컴파일러가 복사 연산을 반드시 제거하지는 않는다. 하지만 이런 경우라면 반환되는 객체는 반드시 오른값으로 취급되어야 한다고 정해져 있으므로 굳이 우리가 명시적으로 std::move를 사용할 필요는 없다.
어차피 RVO가 적용되는경우 복사 연산이 제거되거나 그렇지 않더라도 암묵적으로 반환값에 std::move 연산을 수행해주기 때문이다.
◾ 오른값 참조나 보편 참조가 마지막으로 쓰이는 지점에서, 오른값 참조에는 std::move를, 보편 참조에는 std::forward를 적용하라.
◾ 결과를 값 전달 방식으로 돌려주는 함수가 오른값 참조나 보편 참조를 돌려줄때에도 각각 std::move나 std::forward를 이용하라.
◾ 반환값 최적화(RVO)의 대상이 될 수 있는 지역 객체에는 절대로 std::move나 std::forward를 적용하지 말아야 한다.
'도서 > Effective Modern C++' 카테고리의 다른 글
[5장] 항목 27 : 보편 참조에 대한 오버로딩 대신 사용할 수 있는 기법들을 알아두라 (0) | 2023.01.25 |
---|---|
[5장] 항목 26 : 보편 참조에 대한 오버로딩을 피하라 (0) | 2023.01.25 |
[5장] 항목 24 : 보편 참조와 오른값 참조를 구별하라 (0) | 2023.01.24 |
[5장] 항목 23 : std::move와 std::forward를 숙지하라 (0) | 2023.01.24 |
[4장] 항목 22 : Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라 (0) | 2023.01.24 |