결과적으로 그 멤버 초기화 목록은 std::string의 복사 생성자를 호출합니다. text를 오른 값으로 캐스팅했어도 그런 일이 벌어집니다! 이런 행동 방식은 const 정확성을 유지하기 위해 꼭 필요한 것입니다. 일반적으로 한 객체의 어떤 값을 바깥으로 이동하면 그 객체는 수정되며, 따라서 전달된 객체를 수정할 수도 있는 함수(이동 생성자가 그런 함수에 속한다)에 const 객체를 전달하는 일을 C++ 언어가 방지하는 것은 당연한 일입니다.
이 예에서 배울 점이 두 가지 있습니다. 첫째로, 이동을 지원할 객체는 const로 선언하지 말아야 합니다. const 객체에 대한 이동 요청은 소리 없이 복사 연산으로 변환됩니다. 둘째로, std::move는 아무것도 실제로 이동하지 않을 뿐만 아니라, 캐스팅되는 객체가 이동 자격을 갖추게 된다는 보장도 제공하지 않습니다. 확실한 것은, 객체에 std::move를 적용한 결과가 하나의 오른 값이라는 것뿐입니다.
std::forward에 대해서도 std::move와 비슷한 이야기가 적용됩니다. 단, std::move는 주어진 인수를 무조건 오른값으로 캐스팅합니다. std::forward는 특정 조건이 만족될 때에만 캐스팅합니다. std::forward는 조건부 캐스팅입니다. 캐스팅이 적용될 때와 안 될 때를 이해하려면 std::forward의 전형적인 용법을 살펴보는 것이 도움이 될 것입니다. 가장 흔한 시나리오는 보편 참조 매개변수를 받아서 그것을 다른 어떤 함수에 전달하는 함수입니다.
void process(const Widget& lvalArg); // 왼값들을 처리하는 함수
void process(Widget&& rvalArg); // 오른값들을 처리하는 함수
template<typename T> // param을 process에 넘겨주는 템플릿
{
auto now = std::chrono::system_clock::now(); // 현재 시간을 얻는다
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
이제 logAndProcess를 한 번은 왼값으로, 또 한 번은 오른 값으로 호출해봅시다.
Widget w;
logAndProcess(w); // 왼값으로 호출
logAndProcess(std::move(w)); // 오른 값으로 호출
자신의 내부에서 logAndProcess는 주어진 param을 함수 process에 전달합니다. process는 왼 값과 오른 값에 대해 중복 적재되어 있습니다. logAndProcess를 왼 값으로 호출하면 그 왼 값이 왼 값을 받는 process로 전달되고, logAndProcess를 오른 값으로 호출하면 그 오른 값이 오른 값을 받는 process로 전달되리라고 기대하는 것이 당연합니다.
그러나 다른 모든 함수 매개변수처럼 param은 하나의 왼값입니다. 따라서, logAndProcess 내부에서 일어나는 모든 process 호출은 결국 proecess의 왼 값 중복 적재 버전을 실행하게 됩니다. 이를 방지하기 위해서는, 만일 애초에 param을 초기화하는 데 쓰인 인수(즉, logAndProcess에 전달된 인수)가 오른 값이면, 그리고 오직 그럴 때에만, param을 오른 값으로 캐스팅하는 어떤 메커니즘이 필요합니다. std::forward가 하는 일이 바로 그것입니다. 그리고, 이처럼 주어진 인수가 오른 값으로 초기화된 것일 때에만 그것을 오른 값으로 캐스팅한다는 점에서 std::forward를 조건부 캐스팅이라고 부릅니다.
인수가 오른값으로 초기화되었는지를 std::forward가 어떻게 아는지 궁금한 독자도 있을 것입니다. 예를 들어 앞의 예제에서, std::forward는 param이 왼 값으로 초기화되었는지 오른 값으로 초기화되었는지를 어떻게 알까요?? 간단한 답은, 그 정보가 logAndProcess의 템플릿 매개변수 T에 부호화(encoding)되어 있다는 것입니다. 그 매개변수는 std::forward로 전달되며, std::forward는 거기서 해당 정보를 복원합니다. std::forward가 이를 수행하는 구체적인 방법은 Chapter (28)을 보기 바랍니다.
std::move와 std::forward 둘 다 결국 캐스팅만 수행하는 함수이고 둘의 유일한 차이는 std::move는 항상 캐스팅하지만, std::forward는 조건에 따라서만 한다는 점을 생각하면, std::move는 아예 잊어버리고 항상 std::forward만 사용하면 되지 않느냐는 의문이 들 수도 있겠습니다. 순수하게 기술적인 관점에서는, 그래도 된다는 것이 답입니다. 즉, std::forward로 모든 것을 할 수 있습니다. std::move는 필요하지 않습니다. 물론 두 함수 모두 진정한 의미에서의 필수는 아닙니다. 필요하면 독자가 직접 캐스팅을 하면 그만이기 때문입니다. 그러나 그런 일은 피하는 것이 좋다는 점에 독자도 나처럼 동의하리라고 믿습니다.
std::move의 매력은 사용하기 편하고, 오류의 여지가 줄어들고, 코드의 명확성이 높아진다는 것입니다. 어떤 클래스의 이동 생성자가 호출된 횟수를 추적하고 싶다고 합시다. 그러려면 그냥 이동 생성 도중에 클래스 정적(static) 카운터 변수를 증가하면 됩니다. 클래스의 비정적 자료 멤버가 std::string 하나뿐이라 할 때, 다음은 이동 생성자를 구현하는 통상적인 방식(즉, std::move를 사용하는)을 보여줍니다.
class Widget {
public:
Widget(Widget&& rhs)
: s(std::move(rhs.s))
{ ++moveCtorClls; }
...
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에 전달하는 형식이 반드시 참조가 아니어야 한다는 점도 주목하기 바랍니다. 그것이 전달되는 인수가 오른 값 임을 부호화하는 데 쓰이는 관례이기 때문입니다(Chapter (28) 참고). 정리하자면, std::move 쪽이 std::forward보다 타자량이 적고, 전달하는 것이 오른 값이라는 정보를 부호화하는 형식 인수를 지정하는 번거로움도 없습니다. 또한, 잘못된 형식을 지정하는 실수를 저지를 여지도 없습니다(예를 들어 실수로 std::string&를 지정하면 자료 멤버 s가 이동 생성이 아니라 복사 생성됩니다).
더욱 중요하게는, std::move를 사용한다는 것은 주어진 인수를 무조건 오른값으로 캐스팅하겠다는 뜻이지만 std::forward를 사용한다는 것은 오른 값에 묶인 참조만 오른 값으로 캐스팅하겠다는 뜻입니다.
그 둘은 아주 다른 동작들입니다. 전자는 일반적으로 하나의 이동을 준비하는 반면, 둘째 것은 그냥 객체를 원래의 왼값 또는 오른 값 성질을 유지한 채로 다른 함수에 그냥 넘겨주는, 즉 전달하는(forward) 것입니다.
두 동작이 이처럼 다르므로, 둘에 대해 서로 구별되는 함수를(그리고 함수 이름을) 두는 것은 바람직한 일입니다.
*** Key Point ***
- std::move는 오른값으로의 무조건 캐스팅을 수행합니다. std::move 자체는 아무것도 이동하지 않습니다.
- std::forward는 주어진 인수가 오른 값에 묶인 경우에만 그것을 오른 값으로 캐스팅합니다.
- std::move와 std::forward 둘 다, 실행시점에서는 아무 일도 하지 않습니다.
'컴퓨터과학' 카테고리의 다른 글
C++ (24-1) 보편 참조와 오른값 참조를 구별할 것 (0) | 2020.06.30 |
---|---|
C++ (23-1) std::move와 std::forward를 숙지할 것 (0) | 2020.06.28 |
C++ (22-4) Pimpl 관용구를 사용할 떄에는 특수 멤버 함수들을 구현 파일에서 정의할 것 (0) | 2020.06.26 |
C++ (22-3) Pimpl 관용구를 사용할 떄에는 특수 멤버 함수들을 구현 파일에서 정의할 것 (0) | 2020.06.25 |
C++ (22-2) Pimpl 관용구를 사용할 떄에는 특수 멤버 함수들을 구현 파일에서 정의할 것 (0) | 2020.06.24 |