Effective C++/Effective Modern C++
[Effective Modern C++] 25. rValue에는 move, 보편 참조에는 forward
김디트
2025. 3. 12. 11:35
728x90
항목 25. rValue에는 std::move를, 보편 참조에는 std::forward를 사용하라
move와 forward
- rValue 참조의 경우 다른 함수로 이동시킬 수 있음이 확실하다.
- std::move를 사용하여 rValue로 캐스팅 해주어야 한다.
class Widget
{
public:
Widget(Widget&& rhs) : name(std::move(rhs.name)),
p(std::move(rhs.p))
{ ... }
...
private:
std::string name;
std::hsared_ptr<SomeDataStructure> p;
};
- 보편 참조는 이동 가능한 객체일 수도 있고 아닐 수도 있다.(항목 24 참조)
- std::forward를 사용해 주어야 한다.
class Widget
{
public:
template<typename T>
void setName(T&& newName)
{
name = std::forward<T>(newName);
}
...
};
rValue에 forward를 사용하면 안되는 이유
- 소스 코드가 장황하고 실수의 여지가 있다.
- forward에는 객체의 타입을 명시해야 하기 때문이다.
- 관용구에서 벗어난 모습이 된다.
보편 참조에 std::move를 사용하면 안 되는 이유
- lValue가 의도치 않게 수정될 수도 있다.
class Widget
{
public:
template<typename T>
void setName(T&& newName) // 보편 참조
{
new = std::move(newName); // 컴파일이 되지만 아주 잘못된 코드
}
...
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
std::string getWidgetName(); // 팩토리 함수
Widget w;
auto n = getWidgetName();
w.setName(n); // n이 바뀔 걸 가정하고 넘기지 않지만..
...
- n은 setName 내부적으로 std::move를 통해 이동한다.
- 다시 호출 지점으로 돌아갔을 때, n은 미지정 값(unspecified value)을 가지게 된다!
위 코드 고쳐보기
- setName은 자신의 매개변수를 수정하지 말아야 하므로 const를 명시해야 할테지만!
- 보편 참조는 const일 수 없다.(항목 24 참조)
- 그럼 템플릿 버전을 사용하지 말고 std::string 명시 버전을 두가지 준비하면 되지 않나?
class Widget
{
public:
void setName(const std::string& newName)
{
name = newName;
}
void setName(std::string&& newName)
{
name = std::move(newName);
}
...
};
- 하지만 단점이 있다.
- 템플릿 함수가 비템플릿 함수들로 변하면서 유지 보수해야 할 코드의 양이 늘어났다.
- 효율성이 떨어질 수 있다.
-
w.setName("Adela Novak");
- 다음처럼 사용할 경우 std::string 버전 함수밖에 없으므로 임시 std::string를 생성하게 된다.
- 임시 생성된 것이 이동 할당되고 임시 생성된 string 소멸자가 불리는 비용이 발생한다.
- 템플릿일 때의 const char* 포인터를 받아서 std::string 할당 연산하는 것보다는 확실히 비싸다.
-
- 설계의 규모가 변성(scalability)이 나쁘다.
- 매개변수가 더 많고 각 매개변수가 lValue일 수도, rValue일 수도 있어야 한다면,
- 오버라이드 함수의 수가 기하급수적으로 늘어난다.
- 함수 템플릿 중에선 명시적인 오버라이드 함수로 대체할 수 없는 형태가 존재한다.
-
template<class T, class...Args> shared_ptr<T> make_shared(Args&&... args);
- 함수 템플릿은 다음처럼 매개변수들을 무제한으로 받을 수 있다.
-
- 결국 가장 좋은 방법은 보편 참조와 std::forward 짝을 맞춰서 사용하는 것이다.
rValue 참조나 보편 참조 객체를 한 함수에서 여러번 사용할 때 주의사항
- 그 객체를 다 사용하기 전에 다른 객체로 이동하는 일은 피해야 한다.
template<typename T>
void setSingleText(T&& text)
{
sign.setText(text); // text를 사용하되 수정하진 않는다.
auto now = std::chrono::system_clock::now();
singHistory.add(now, std::forward<T>(text)); // forward 적용
}
- std::move에 대해서도 같은 논리가 적용된다.
- 드물지만 std::move 대신 std::move_if_noexcept를 사용하는 게 바람직할 때도 있다.(항목 14 참조)
함수가 return by value이고, 리턴 대상이 rVlaue나 보편 참조라면 move나 forward 사용하기
Matrix operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // lhs를 반환값으로 이동한다.
}
// 반면 move를 사용하지 않는다면?
Matrix operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return lhs; // lhs를 반환값으로 복사한다.
}
- 이 경우 이동 연산을 지원하지 않으면 그냥 복사될 뿐이므로 해될 일이 없다.
- forward 역시 마찬가지이다.
template<typename T>
Fraction reduceAndCopy(T&& frac)
{
frac.reduce();
return std::forward<T>(frac);
}
- 하지만 지역 객체에 대해서는 move나 forward를 적용하지 말자.
// '복사'를 이동으로 바꿈으로써 함수를 '최적화' 할 수 있지 않을까?
Widget makeWidget()
{
Widget w;
...
return std::move(w); // 최적화에 오히려 방해!
}
- 이 경우 반환값 최적화(return value optimization, RVO)를 되려 방해하게 되기 때문이다.
- makeWidget의 이동 버전이 반환값 최적화가 되지 않는 이유는,
- 반환값 최적화의 2번 조건을 어기기 때문이다.
- return std::move(w) 에서 리턴되는 것은
- w가 아니라 w의 참조이다.
- 그렇다면, 반환값 최적화가 반영되기 어려운 코드라면 std::move를 사용하는 게 좋지 않을까?
- 표준에서는 반환값 최적화 필수 조건들을 만족했지만, 컴파일러가 최적화를 하지 않기로 한 경우,
- 반환되는 객체는 반드시 오른값으로 취급되어야 한다고 나와 있다.
- 즉, 굳이 명시적으로 사용하지 않더라도 암묵적으로 std::move를 적용하게 되어 있다는 뜻이다.
반환값 최적화(return value optimization, RVO)
- 지역 변수를 함수의 반환값을 위해 마련한 메모리 안에 생성하여 복사를 피한다.
- 필수 조건
- 그 지역 객체의 타입이 함수의 반환 타입과 같아야 한다.
- 그 지역 객체가 바로 함수의 반환값이어야 한다.
728x90