std::move는 모든 것을 이동하지는 않고 std::forward는 모든 것을 전달하지는 않는다.
둘 다 그저 캐스팅을 수행하는 함수 템플릿일 뿐이다.
template<typename T>
typename remove_reference<T>::type&& move(T&& param) {
using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
std::move는 T&&가 왼값 참조인 경우를 고려해서 참조자를 제거한뒤 오른값 참조를 붙여서 항상 오른값이 반환되도록 구현되어있다.
전달받은 인수를 그저 오른값으로 캐스팅 하는것뿐이라 move보다 rvalue_cast같은 이름이 더 낫다는 제안도 있었으나 결국은 move라는 이름을 가지게 되었다.
물론 오른값은 이동의 후보가 맞지만 아닌 경우도 존재한다.
class Annotation {
public:
explicit Annotation(const std::string text) : value(std::move(text)) {}
private:
std::string value;
};
그저 text의 내용을 읽기만 할 것이기 때문에 가급적 const를 붙이면 좋다는 점에 따라 const를 붙이고 move를 통해 복사 대신 오른값을 얻으려고 한다.
컴파일도 잘되고 링크도 잘되는데 의도한 동작이 전혀 이뤄지지 않는다.
move때문에 오른값으로 캐스팅 되는것은 확실하지만 text는 const std::string으로 선언되어있기 때문에 캐스팅 이후에는 오른값 const std::string가 될 것이다.
class string {
public:
string(const string& rhs); // 복사 생성자
string(string&& rhs); // 이동 생성자
};
그런데 이동 생성자는 const가 아닌 타입에 대한 오른값 참조를 받고, const에 대한 왼값 참조를 const 오른값에 묶는 것이 허용되기 때문에 복사 생성자가 호출된다.
생각해보면 const 객체의 이동은 지원해선 안된다. 이동이 된다는것은 기존 객체의 내용을 수정하겠다는 것인데 const 객체의 수정이 이루어지면 const 정확성이 유지되지 않는다. 그래서 언어 차원에서 방지하는 것은 당연한 것이다.
이를 통해 알 수 있는것은 이동을 지원할 객체는 const로 선언되어서는 안되고 std::move는 실제로 아무것도 이동시키지 않을 뿐만 아니라 캐스팅된 객체가 이동 자격을 갖추게 된다는 보장도 제공하지 않는다.
그저 확실한 것은 오른값을 확실하게 돌려준다는 것 뿐이다.
std::forward역시 비슷한 개념이 적용된다. 다만 move는 무조건 오른값으로 캐스팅하지만 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(param); // 왼값? 오른값?
}
Widget w;
logAndProcess(w); // 왼값으로 호출
logAndProcess(std::move(w)); // 오른값으로 호출
어떤 함수로 전달된 매개변수의 타입이 오른값 참조라고 하더라도 그 매개변수 자체는 왼값이다.
그래서 위와 같은 경우 호출은 왼값과 오른값으로 구분되었지만 param 자체는 왼값이기 때문에 둘 다 왼값 참조 타입을 매개변수로 받는 process가 호출된다.
이럴 때 logAndProcess 함수에서 process 함수에 param을 인수로 넘겨줄 때 forward를 통해 조건부 캐스팅하여 넘겨주면 본래 가지고 있던 타입으로 전달된다.
...
process(std::forward<T>(param));
...
그럼 forward는 어떻게 인수가 오른값인지를 알 수 있을까?
간단하게 설명하면 해당 정보가 logAndProcess의 템플릿 매개변수 T에 부호화 되어 저장되어 있다.
std::forward로 전달될 때 해당 정보를 복원하여 캐스팅을 수행한다.
결과적으로 move, forward 둘 다 캐스팅을 수행하는 함수라는 점이다.
다만 move는 항상 캐스팅하지만 forward는 조건부 캐스팅이라는 차이가 있다.
move대신 forward만 사용해도 문제가 없지만 여러가지 이유로 권장하지 않는다.
◾ std::move는 오른값으로의 무조건 캐스팅을 수행한다. std::move 자체는 아무것도 이동하지 않는다.
◾ std::forward는 주어진 인수가 오른값에 묶인 경우에만 그것을 오른값으로 캐스팅한다.
◾ std::move와 std::forward 둘 다, 실행시점에서는 아무 일도 하지 않는다.
'도서 > Effective Modern C++' 카테고리의 다른 글
[5장] 항목 25 : 오른값 참조에는 std::move를, 보편 참조에는 std::forward를 사용하라 (0) | 2023.01.25 |
---|---|
[5장] 항목 24 : 보편 참조와 오른값 참조를 구별하라 (0) | 2023.01.24 |
[4장] 항목 22 : Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라 (0) | 2023.01.24 |
[4장] 항목 21 : new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라 (0) | 2023.01.20 |
[4장] 항목 20 : std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라 (0) | 2023.01.19 |