std::move와 std::forward는 캐스팅을 수행하는 함수(함수 템플릿)이다. std::move는 주어진 인수를 무조건 오른 값으로 캐스팅하고, std::forward는 특정 조건이 만족될 때에만 캐스팅을 수행한다.
std::move를 구현해보자
template<typename T> // C++11 버전
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
template<typename T> // C++14 버전, remove_reference_t와 decltype 사용
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
std::move는 하나의 오른값 참조를 돌려준다. 하지만 T가 왼 값 참조일 경우 T&&는 왼 값 참조가 된다. 이를 방지하기 위해 T에 형식 특질 std::remove_reference를 적용한다. 그러면 반환 형식의 &&는 항상 참조가 아닌 형식에 적용된다. 결론적으로 std::move는 자신의 인수를 오른 값으로 캐스팅한다. 사실 실제로 이동하는 것은 없는데 이름에 대한 유래는 '이동할 수 있는 객체를 좀 더 쉽게 지정하기 위한 함수'라고 한다.
오른 값이 이동의 후보가 아닌 경우가 있다. annotation을 나타내는 어떤 클래스의 생성자가 주해의 내용을 구성하는 std::string 매개변수 하나를 받아서 그 매개변수를 자료 멤버에 복사하는 예를 생각해보자.
class Annotation {
public:
explicit Annotation(const std::string text)
: value(std::move(text)) // text를 value로 이동. 하지만 복사됨
{ ... }
private:
std::string value;
};
// std::string 의 생성자
class string { // std::string은 std::basic_string<char>의 typedef
public:
...
string(const string& rhs); // 복사 생성자
string(string&& rhs); // 이동 생성자
...
}
Annotation의 생성자에서 std::move(text)의 결과는 const std::string 형식의 오른 값이다. 그 오른 값은 std::string의 이동 생성자에 전달할 수는 없지만 복사 생성자에 전달할 수 있다. 결과적으로 복사 생성자를 호출하게 된다. 일반적으로 한 객체의 값을 바깥으로 이동하면 그 객체는 수정되며, 전달된 객체를 수정할 수 있는 함수(이동 생성자)에 const 객체를 전달하는 것은 C++ 언어가 방지하게 된다.
여기서 결론은 첫째, 이동을 지원할 객체는 const를 선언해서는 안된다. 둘째, std::move는 실제로 이동하지 않고 결과는 하나의 오른값이라는 점이다.
std::forward도 std::move와 비슷하지만 std::forward는 조건부 캐스팅이다.
std::forward의 가장 흔한 케이스는 보편 참조 매개변수를 받아서 그것을 다른 어떤 함수에 전달하는 것이다.
// 왼값 처리와 오른값 처리를 모두 할 수 있게 중복 적재
void process(const Widget& lvalArg); // 왼값들 처리
void process(Widget&& rvalArg); // 오른값들 처리
template<typename T>
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)); // 오른값 호출
다른 모든 함수 매개변수처럼 param은 하나의 왼값이다. 즉, 모든 process 호출은 왼 값 중복 적재 버전을 실행하게 된다. 이를 방지하기 위해 std::forward를 사용해 오른 값으로 캐스팅한다. std::forward는 주어진 인수가 오른 값으로 초기화된 것일 때에만 그것을 오른 값으로 캐스팅하기 때문에 '조건부 캐스팅'이라고 부른다.
결과만 놓고 보면 사실 std::move 대신 std::forward로 모두 처리할 수 있다. 하지만 std::move는 사용이 편하고, 오류의 여지가 줄어들며, 코드의 명확성이 높아진다. 예를 들어 어떤 클래스의 이동생성자가 호출된 횟구를 추적하고 싶다고 하자. 그러면 이동 생성 도중 클래스 정적(static) 카운터 변수를 증가하면 된다. 클래스의 비정적 자료 멤버가 std::string 하나 뿐이라 할 때, 이동 생성자를 구현하는 통상적인 방식을 보여준다
// 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)) // 관례에 벗어난 구현이 됨
{ ++moveCtroCalls; }
...
};
std::forward에 전달되는 형식이 반드시 참조가 아니어야 하기 때문에 오른값이라는 정보를 부호화 하는 형식 인수를 따로 지정해야 한다. 또한 잘못된 형식을 지정하는 실수를 저지를 여지도 없다. (stdd::string&를 지정하면 자료 멤버 s가 이동 생성이 아닌 복사 생성 됨)
결과적으로 std::move를 사용한다는 것은 주어진 인수를 오른값으로 캐스팅 한다는 것을 의미하지만 std::forward를 사용하는 것은 오른값에 묶인 참조만 오른값으로 캐스팅 하겠다는 뜻이다. std::move는 이동을 준비, std::forward는 객체의 원래 왼값/오른값 성질을 유지한 채로 다른 함수에 전달하는 것이다.
Conclusion
- std::move는 오른값으로의 무조건 캐스팅을 수행한다. std::move 자체는 아무것도 이동하지 않는다.
- std::forward는 주어진 인수가 오른값에 묶인 경우에만 그것을 오른값으로 캐스팅한다
- std::move와 std::forward 둘 다, 실행시점에서는 아무 일도 하지 않는다.
Reference
Effective Modern C++ 항목 23: std::move와 std::forward를 숙지하라
'C++ > Effective Modern C++' 카테고리의 다른 글
보편참조에 대한 중복적재 대신 사용할수 있는 기법들 1 (0) | 2020.07.22 |
---|---|
보편 참조에 대한 중복적재를 피하라 (0) | 2020.07.21 |
Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정리하라 (0) | 2020.07.16 |
new 보다 std::make_shared와 make_unique를 선호하라 (0) | 2020.07.15 |
std::weak_ptr (0) | 2020.07.14 |
댓글