728x90
반응형

std::move와 std::forward는 그냥 캐스팅을 수행하는 함수 템플릿이다.

 

std::move

함수의 이름만 보면 std::move가 이동을 수행해 줄 것처럼 보이지만 위에서 말했던 것처럼 사실 타입 캐스팅이 전부이다.

그런데도 이 함수의 이름이 move인 이유는 rvalue로 캐스팅을 하기 때문이고, 아래의 코드는 std::move를 구현한 코드이다.

// C++11
template <typename T>
typename remove_reference<T>::type&&
move(T&& param)
{
    using ReturnType = typename remove_reference<T>::type&&;
    
    return static_cast<ReturnType>(param);
}

// C++14 더 단순해진 move구현
template<typename T>
decltype(auto) move(T&& param)
{
    using ReturnType = remove_reference_t<T>&&;
    return static_cast<ReturnType>(param);
}

항목 28에서 설명하듯이, 타입 T가 하필 좌측값 참조이면 T&&는 좌측값 참조가 되는데 이를 방지하기 위해 이 구현은 T에 타입 특성(항목 9 참고) std::remove_reference를 적용한다.

그러면 반환 타입의 &&는 항상 참조가 아닌 타입에 적용되며, 결과적으로 std::move는 반드시 우측값 참조를 돌려준다.

 

하지만 std::move함수를 사용하더라도 const T 타입은 이동이 되지 않고 복사가 된다.

왜냐하면 이동 생성자는 const가 아닌 T에 대한 우측값 참조를 받기 때문에 const T 타입의 우측값을 받을 수 없지만, const에 대한 좌측값 참조를 const 우측값에 묶는 것이 허용되기 때문에 복사 생성자에 전달할 수는 있다.

아래의 예제를 봐보자.

class Annotation
{
public:
    explicit Annotation(const std::string text)
    : value(std::move(text))
    { ... }
    ...
    
private:
    std::string value;
};

text를 value로 이동시키려고 했지만 실제로는 복사가 되는데, 그 이유는 const 때문이다.

class string
{
public:
    ...
    string(const string& rhs);
    string(string&& rhs);
    ...
};

std::move(text)의 결과는 const std::string 형식의 오른값이다. 이 값은 이동 생성자에 전달이 불가능한데, 위에서 보는 것처럼 이동 생성자는 const타입이 아니기 때문이다. 그러나 복사 생성자에는 전달이 가능하고 따라서 복사 생성자가 호출되게 된다.

결과적으로 우리가 알지 못하게 복사 연산으로 변환될 수 있기 때문에 이동을 지원할 객체는 const로 선언하지 말아야 하며 std::move는 아무것도 실제로 이동하지 않고, 캐스팅되는 객체가 이동 자격을 갖춘다는 보장도 제공하지 않는다는 것을 알 수 있다.

 

std::forward

std::forward와 std::move는 거의 같은 역할을 한다. 차이가 있다면 std::forward는 조건부 캐스팅을 한다는 것이다.

std::forward는 주어진 인수가 우측값에 묶인 경우에만 우측값으로 캐스팅한다.

아래의 예시를 봐보자.

template <typename T> // param을 process에 넘겨주는 템플릿
void logAndProcess(T&& param) 
{
    auto now = std::chrono::system_clock::now();
    makeLogEntry("Calling 'process'", now);
    process(std::forward<T>(param));
}

Widget w;
logAndProcess(w); // 좌측값으로 호출
logAndProcess(std::move(w)); // 우측값으로 호출

보이는 것처럼 좌측값으로 호출한 경우 logAndProcess는 좌측값들을 처리하는 process함수를 호출하고, 우측값으로 호출한 경우 우측값들을 처리하는 process함수를 호출한다.

 

다른 함수 매개변수처럼 param은 좌측값이므로 std::forward를 사용하지 않는다면, 항상 좌측값들을 처리하는 process 함수가 호출될 것이다.

std::forward는 param을 초기화하는데 쓰인 인수가 우측값인 경우에만 param을 우측값으로 캐스팅하는데, std::forward는 인수가 어느 값인지 함수(logAndProcess)의 템플릿 매개변수 T에 부호화(encoding)되어 있어 그 매개변수가 std::forward로 전달되며, std::forward는 거기서 해당 정보를 복원한다.(항목 28 참고)

 

std::move를 사용하는 이유

std::move와 std::forward는 둘 다 캐스팅만 수행하는 함수라는 공통점이 있지만, 차이점은 std::move는 항상 우측값을 캐스팅하고 std::forward는 조건에 따라서 캐스팅하는 값이 변경된다는 점이다.

// std::move 사용
class Widget
{
public:
    Widget(Widget&& rhs)
        : s(std::move(rhs.s))
    {
        ++moveCtorCalls;
    }

private:
    static std::size_t moveCtorCalls;
    std::string s;
};

// std::forward 사용
class Widget 
{
public:
    // 관례에서 벗어난, 바람직하지 않은 코드
    Widget(Widget&& rhs) : s(std::forward<std::string>(rhs.s))
    {
        ++moveCtorCalls;
    }
};

std::move를 사용하면 함수 인수(rhs.s)만 지정하면 되었지만 std::forward는 함수 인수(rhs.s)와 템플릿 타입 인수(std::string) 둘 다 지정해야 한다는 걸 보자.

그리고 std::forward에 전달하는 타입이 반드시 참조가 아니어야 한다는 점도 있다.

std::move의 장점은 std::forward보다 타자량이 적고, 전달하는 것이 오른값이라는 정보를 부호화하는 형식 인수를 지정하는 번거로움도 없으며 잘못된 형식을 지정하는 실수를 저지를 여지도 없다.(예를 들어 실수로 std::string&을 지정하면 자료 멤버 s가 이동 생성이 아니라 복사 생성된다.)

그리고 std::move를 사용한다는 것은 주어진 인수를 무조건 오른값으로 캐스팅한다는 뜻이지만 std::forward를 사용한다는 것은 오른값에 묶인 참조만 오른값으로 캐스팅하겠다는 뜻이다.

결론적으로 std::move는 하나의 이동을 준비하는 반면에, std::forward는 그냥 객체를 원래의 왼값 또는 오른값 성질을 유지한 채로 다른 함수에 그냥 전달하는 것이다.

 

요약

  • std::move는 우측값으로의 무조건 캐스팅을 수행한다. std::move 자체는 아무것도 이동하지 않는다.
  • std::forward는 주어진 인수가 우측값에 묶인 경우에만 그것을 우측값으로 캐스팅한다.
  • std::move와 std::forward 둘 다, 실행 시점에서는 아무 일도 하지 않는다.
728x90
반응형