KoreanFoodie's Study

Effective Modern C++ | 항목 23 : std::move 와 std::forward 를 숙지하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 23 : std::move 와 std::forward 를 숙지하라

GoldGiver 2022. 10. 26. 09:58

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 23 : std::move 와 std::forward 를 숙지하라

핵심 :

1. std::move 는 오른값으로의 무조건 캐스팅을 수행한다. std::move 자체는 아무것도 이동하지 않는다.
2. std::forward 는 주어진 인수가 오른값에 묶인 경우에만 그것을 오른값으로 캐스팅한다.
3. std::move 와 std::forward 둘 다, 실행 시점에서는 아무 일도 하지 않는다.

 

std::move 와 std::forward 는 실행 가능 코드를 한 바이트도 산출하지 않는다. 그들은 그냥 캐스팅을 수행하는 함수(구체적으로는 함수 템플릿)이다. std::move 는 주어진 인수를 무조건 오른값으로 캐스팅하고, std::forward 는 특정 조건이 만족될 때에만 그런 캐스팅을 수행한다. 사실 이게 전부이다.

예를 들어, C++11 과 C++14 의 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 버전
template<typename T>
decltype(auto) move(T&& param)
{
	using ReturnType = 
		remove_reference_t<T>::type&&;

	return static_cast<ReturnType>(param);
}

위에서 본 것처럼, std::move 는 어떤 객체에 대한 참조(정확히 말하면 보편 참조)를 받아서 같은 객체에 대한 오른값 참조를 돌려준다. 하지만 만약 T 가 왼값 참조이면 T&& 가 왼값 참조가 되므로, 이를 방지하기 위해 std::remove_reference 를 적용했다.

 

이번에는 오른값이 이동의 후보가 아닌 경우를 한 번 보자.

class Annotation {
public:
	explicit Annotation(const std::string text)
	: value(std::move(text))
	{ ... }

private:
	std::string value;
};

위에서 text 를 value 로 '이동'시키려는 동작이 시도되고 있지만, 실제로는 text 가 value 로 이동하지 않고 복사된다. 왜일까?

위의 경우, 다음과 같이 string 의 생성자 두 가지 중 하나를 선택하게 된다.

string(const string& rhs); // 복사 생성자
string(string&& rhs); // 이동 생성자

std::move(text) 의 결과는 const std::string 형식의 오른값인데, 이 오른값은 std::string 의 이동 생성자에 전달할 수 없다(const 가 아닌 std::string 에 대한 오른값 참조이기 때문). 반면 const 에 대한 왼값 참조를 const 오른값에 묶는 것은 허용되므로, 최종적으로는 std::string 의 복사 생성자가 호출된다.

즉, 이동을 지원할 개체는 const 로 선언하지 말아야 하며, std::move 는 캐스팅되는 객체가 이동 자격을 갖추게 된다는 보장도 제공하지 않는다는 점을 알 수 있다.

 

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)); // 오른값 호출

 

그렇다면 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::forward 는 템플릿 형식 인수(std::string) 도 지정해야 하며, std::forward 에 전달하는 형식이 반드시 참조가 아니어야 한다. 예를 들어 실수로 std::string& 을 지정하면 자료 멤버 s 가 이동 생성이 아니라 복사 생성된다.

Comments