KoreanFoodie's Study

C++ 기초 개념 12-2 : Move 문법, 완벽한 전달과 레퍼런스 겹침 본문

Tutorials/C++ : Beginner

C++ 기초 개념 12-2 : Move 문법, 완벽한 전달과 레퍼런스 겹침

GoldGiver 2022. 4. 19. 10:23

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 는 말 그대로 타입의 레퍼런스를 제거해주는 역할을 수행한다.

Comments