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);
    }
    ...
};
  • 하지만 단점이 있다.
    1. 템플릿 함수가 비템플릿 함수들로 변하면서 유지 보수해야 할 코드의 양이 늘어났다.
    2. 효율성이 떨어질 수 있다.
      • w.setName("Adela Novak");
         
      •  
      • 다음처럼 사용할 경우 std::string 버전 함수밖에 없으므로 임시 std::string를 생성하게 된다.
      • 임시 생성된 것이 이동 할당되고 임시 생성된 string 소멸자가 불리는 비용이 발생한다.
      • 템플릿일 때의 const char* 포인터를 받아서 std::string 할당 연산하는 것보다는 확실히 비싸다.
    3. 설계의 규모가 변성(scalability)이 나쁘다.
      • 매개변수가 더 많고 각 매개변수가 lValue일 수도, rValue일 수도 있어야 한다면,
      • 오버라이드 함수의 수가 기하급수적으로 늘어난다.
    4. 함수 템플릿 중에선 명시적인 오버라이드 함수로 대체할 수 없는 형태가 존재한다.
      • 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)
  • 지역 변수를 함수의 반환값을 위해 마련한 메모리 안에 생성하여 복사를 피한다.
  • 필수 조건
    1. 그 지역 객체의 타입이 함수의 반환 타입과 같아야 한다.
    2. 그 지역 객체가 바로 함수의 반환값이어야 한다.
728x90