Item 23. std::move와 std::forward를 숙지해라


std::move

std::move는 lvalue를 rvalue로 캐스팅(타입 변환)하는 함수이다.

std::move는 말처럼 값을 이동시키는 것이 아니라 단순히 타입을 변환해주는 함수이다.

하지만 std::move를 통해 함수에 lvalue 객체를 rvalue 인자로 넘겨줄 수 있어서 복사 연산이 아닌 이동 연산을 수행할 수 있다. (이동 생성자 혹은 이동 대입 연산자를 통해 이동 연산이 수행된다.)

std::move의 내부 구현을 알아보자.

// namespace std 안에서 구현했다고 가정

// 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
template <typename T>
decltype(auto) move(T&& param) {
  using ReturnType = remove_reference_t<T>&&;
  return static_cast<ReturnType>(param);
}

C++11보다 C++14 스타일이 더욱 간결하지만 핵심적인 내용은 같다.

템플릿 타입 추론에 따라 universal reference로 받은 인자가 lvalue일 경우 타입은 T&로 추론되고 rvalue일 경우 타입은 T로 추론된다.

이때 std::move 함수는 rvalue를 반환해야 하므로 리턴 타입은 T&& 형태여야 한다.

하지만 인자의 타입이 T&로 추론된 상태에서는 reference collapsing 법칙에 의해 리턴 타입이 lvalue reference(T& && = T&)가 되는 문제가 발생한다.

이를 방지하기 위해 리턴 타입에는 추론된 인자 타입의 reference를 제거하는 함수(remove_reference<T>::type, remove_reference_t<T>)를 함께 사용한다.


std::move 사용 시 주의할 점

이동에 사용할 객체는 const로 선언하면 안된다.

const std::string 타입을 생성자의 인자로 받아서 멤버 변수에게 std::move로 넘겨주는 Annotation 클래스가 있다고 가정해보자.

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

private:
  std::string value;
};

std::move를 사용해서 std::string 생성자를 호출했음에도 불구하고 이동 연산이 아닌 복사 연산이 일어난다.

그 이유는 const 때문이다.

const lvalue인 textstd::move를 통해 const rvalue로 캐스팅해서 std::string 생성자의 인자로 넘겨주면 어떤 생성자가 호출될까?

class string {
public:
  string(const string& rhs); // 복사 생성자
  string(string&& rhs);      // 이동 생성자
}

이동 생성자는 매개변수인 non-const rvalue reference가 const rvalue를 참조할 수 없어서 호출되지 않는다.

하지만 복사 생성자는 매개변수인 const lvalue reference가 const rvlaue를 참조할 수 있어서 호출된다.

따라서 이동 연산을 하고 싶은 객체는 const로 선언하면 안 된다.

또한, std::move는 단순히 타입을 변환해주는 함수이지 그 자체로 이동 연산이 발생하지 않는다는 사실을 명심해야 한다.


std::forward

std::forward는 lvalue를 lvalue로 rvalue를 rvalue로 캐스팅(타입 변환)하는 함수이다.

std::move이 주어진 인자를 무조건 rvalue로 캐스팅한다면 std::forward는 특정 조건이 만족할 때만 rvalue로 캐스팅한다.

std::forward는 universal reference를 사용하는 템플릿 함수에서 자주 사용한다.

lvalue, rvalue 타입을 모두 인자로 받을 수 있는 상황에서 rvalue reference는 lvalue로 취급받기 때문에 다른 함수로 넘겨주기 위해서는 rvalue 캐스팅이 필요하다.

std::forward의 내부 구현은 Item 28에서 이야기하고 간단한 사용법을 알아보자.

void process(const Widget& lvalArg);
void process(Widget&& rvalArg);

template <typename T>
void logAndProcess(Widget&& param) {
  auto now = std::chrono::system_clock::now();
  makeLogEntry("Calling process", now);
  process(std::forward<T>(param));
}

Widget w;
logAndProcess(w);            // lvalue로 호출
logAndProcess(std::move(w)); // rvalue로 호출

logAndProcess 함수의 매개변수 param은 언제나 lvalue이다.

만약 logAndProcess 함수의 인자가 rvalue인 경우 process 함수로 인자를 정확하게 전달하기 위해서 std::forward를 사용해야 한다.

std::forward를 사용하면 lvalue는 lvalue 인자로 rvalue는 rvalue 인자로 process 함수에 전달할 수 있다.

정리하면 std::move는 이동 연산을 위해서 사용한다면 std::forward는 객체의 lvalue, rvalue 성질을 유지한 채로 다른 함수에 전달해주기 위해 사용한다.


카테고리:

업데이트: