KoreanFoodie's Study
C++ 기초 개념 12-2 : Move 문법, 완벽한 전달과 레퍼런스 겹침 본문
Move 문법 (Move semantics)
Swap function을 구현해보자. 일반적인 타입 T에 대한 Swap function은 다음과 같이 구현할 수 있다.
template <typename T>
void swap(const T& a, const T& b) {
T tmp(a);
a = b;
b = tmp;
}
이 경우, T에서 a를 호출하는 과정에서 복사 생성자가 호출된다.
이 문제를 해결하기 위해, <utility>에서 제공하는 move 함수를 사용한다.
template <typename T>
void swap(T& a, T& b) {
T tmp = std::move(a);
b = std::move(a);
a = std::move(tmp);
}
위와 같은 방식으로 swap 함수를 구현하면, swap이 실제로 행해지는 과정에서 이동 생성자가 호출된다. move는 좌측값을 우측값 레퍼런스처럼 사용할 수 있도록 타입 캐스팅을 해 준다.
b = std::move(a)에서, 해당하는 타입(T)의 클래스에 이동 대입 연산자가 정의되어 있어야 우측값 레퍼런스가 제대로 동작할 수 있다. (아래 예시 코드 참조)
MyString& MyString::operator=(const MyString&& str) {
string_content = str.string_content;
str.string_content = nullptr;
return *this;
}
완벽한 전달 (perfect forwarding)
우측값 레퍼런스를 도입함으로써 해결된 문제가 또 하나 있다.
우리가 템플릿 타입을 이용한 wrapper class를 정의했다고 가정해 보자.
class A {};
g(A& a) { std::cout << "좌측값 레퍼런스 호출" << std::endl; }
g(const A& a) { std::cout << "좌측값 상수 레퍼런스 호출" << std::endl; }
g(A&& a) { std::cout << "우측값 레퍼런스 호출" << std::endl; }
template <typename T>
void wrapper(T a) {
g(a);
}
위와 같은 wrapper function은, 템플릿 타입 T가 레퍼런스 타입이 아닐 경우, const를 무시하게 된다. 따라서 아래와 같은 코드는 모두 좌측값 레퍼런스를 호출한다.
A a;
const A ca;
wrapper(a);
wrapper(ca);
wrapper(A());
만약 wrapper function의 타입을 T에서 T&로 바꾸어 준다면, wrapper(A()); 에서 에러가 발생한다. 이는 A& 가 우측값 레퍼런스가 될 수 없기 때문에 생긴 문제이다.
보편적 레퍼런스 (Universal Reference)
위의 타입 문제들을 해결하기 위해, C++에서는 보편적 레퍼런스를 도입하였다.
template <typename T>
void wrapper(T&& u) {
g(std::forward<T>(u));
}
forward를 사용하면, 좌측값이 오든 우측값이 오든 레퍼런스 겹침 규칙(reference collapsing rule)에 따라 올바른 타입을 받아낼 수 있다.
- 만일 wrapper 의 인자가 좌측값으로 들어오면, T 는 T& 가 된다.
- 만일 wrapper 의 인자가 우측값으로 들어오면, T 는 T 가 된다.
레퍼런스 겹침 규칙에 따르면, A&&& 같은 것을 어떻게 처리할 수 있을까? &를 1, &&을 0이라고 두고, &과 &&에 OR연산을 취한다고 가정해 보자. 즉, T&&에 A&가 들어온다면 최종 타입은 A&가 될 것이고, T&&에 A&&가 들어온다면, 최종 타입은 A&&가 된다.
따라서 wrapper를 위와 같이 정의한 후, 아래 코드를 실행시키면 각각의 생성자가 본래 목적의 타입의 생성자를 호출한다.
// 원본 코드
X x;
factory<A>(x);
// 인스턴스화되는 버전
shared_ptr<A> factory(X&&& arg) {
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X&&& forward(remove_reference<X&>::type& a) noexcept {
return static_cast<X&&&>(a);
}
인스턴스화된 버전에 remove_reference 를 수행하고 겹침 규칙을 적용하면,
shared_ptr<A> factory(X& arg) {
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& forward(remove_reference<X&>::type& a) noexcept {
return static_cast<X&>(a);
}
이렇게 바뀌게 되어, 의도대로 동작한다!
std::move 는 인자로 받은 변수를 우측값처럼 활용하도록 만든다. 아래는 std::move 의 구현이다!
template <class T>
typename remove_reference<T>::type&& std::move(T&& a) noexcept {
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
remove_reference 는 말 그대로 타입의 레퍼런스를 제거해주는 역할을 수행한다.
'Tutorials > C++ : Beginner' 카테고리의 다른 글
C++ 기초 개념 13-2 : shared_ptr 와 weak_ptr (0) | 2022.04.19 |
---|---|
C++ 기초 개념 13-1 : RAII 패턴과 unique_ptr (0) | 2022.04.19 |
C++ 기초 개념 12-1 : 우측값, 이동 생성자와 우측값 레퍼런스 (0) | 2022.04.19 |
C++ 기초 개념 11 : 예외처리 (0) | 2022.04.19 |
C++ 기초 개념 10-4 : C++ 문자열의 모든 것 (string과 string_view) (0) | 2022.04.18 |