[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
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 둘 다, 실행 시점에서는 아무 일도 하지 않는다.
'Books > Effective Modern C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 39. 일회성(one-shot) 사건 통신에는 void future 객체를 고려하라
[Effective Modern C++] 39. 일회성(one-shot) 사건 통신에는 void future 객체를 고려하라
2022.10.23 -
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
2022.10.23 -
[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
2022.09.18 -
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
2022.09.18