[EMC++] Item 23. std::move와 std::forward를 숙지해라
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인 text
를 std::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 성질을 유지한 채로 다른 함수에 전달해주기 위해 사용한다.