1편, 2편에서 이어집니다.

문법 정리

lvalue, rvalue, references, 이동생성자, 이동연산자

lvalue는 주소값을 &을 붙여 알아낼 수 있고 rvalue는 그렇지 않다.

대개 문법에서 lvalue가 왼쪽에 rvalue가 오른쪽에 놓이지만 꼭 그런것은 아니다.

& 하나만을 이용해 참조를 만드는 문법을 좌측값 레퍼런스(lvalue reference)라고 한다.

하지만 const가 붙어서 const T& 같은 인자는 rvalue도 참조로 받을 수 있다.

복사 생성자가 유의미하게 많이 호출되어 성능상 문제가 생기는 것을 방지하기 위해 이동연산이라는 것이 c++11 부터 언어적으로 지원되었다.

이는 rvalue references와 관련이 되어있다.

&& 처럼 사용된다. && 는 rvalue references의 의미이다.

class MyString {
private:
    char* buffer;
public:
    // 이동 생성자
    MyString(MyString&& other) noexcept : buffer(other.buffer) {
        other.buffer = nullptr;
    }
    // 이동 대입 연산자
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] buffer;
            buffer = other.buffer;
            other.buffer = nullptr;
        }
        return *this;
    }
    // 기타 생성자와 대입 연산자 등...
};

move 함수

c++11부터 std::move 를 이용해 lvalue를 rvalue로 바꿀 수 있다.

C++ 의 원저자인 Bjarne Stroustroup 은 move 라고 이름을 지은 것을 후회했다고 합니다. 정확히 말하면 move 함수는 move 를 수행하지 않고 그냥 우측값으로 캐스팅만 하기 때문이죠! 더 적절한 이름은 rvalue 와 같은 것이 되겠습니다.

이는 다음과 같이 쓸 데 없이 복사가 3번 일어나는 swap 함수에서 성능 최적화를 할 수 있다.

template <typename T>
void my_swap(T &a, T &b) {
  T tmp(a);
  a = b;
  b = tmp;
}

이제 std::move를 쓴다면

template <typename T>
void my_swap(T &a, T &b) {
  T tmp(std::move(a));
  a = std::move(b);
  b = std::move(tmp);
}

만약 어떤 B안에 A객체가 멤버변수로 있고 그것에 대한 이동생성자를 B의 생성자에서 호출해주기 위해선 B의 생성자에서도 A의 객체를 rvalue로 받고 A를 생성해줄 때도 rvalue로 캐스팅해주어야 한다.

#include <iostream>

class A {
 public:
  A() { std::cout << "ctor\n"; }
  A(const A& a) { std::cout << "copy ctor\n"; }
  A(A&& a) { std::cout << "move ctor\n"; }
};

class B {
 public:
  B(A&& a) : a_(std::move(a)) {}

  A a_;
};

int main() {
  A a;
  std::cout << "create B-- \n";
  B b(std::move(a));
}

Universal References & Perfect Forwarding & Reference Collapsing Rule

&&는 rvalue references를 의미한다고 하였지만 이것이 템플릿 인자에 사용되면 Universal References가 된다.

Universal References는 lvalue, rvalue와 관련없이 참조를 받을 수 있고, 그것을 std::forward로 Perfect Forwarding을 써서 참조 타입을 정확히 유지하며 그대로 다른곳으로 넘길 수 있다.

여기서 std::move 를 쓰지 않는 이유는 lvalue에 std::move를 쓰면 rvalue로 캐스팅이 되어버리기 때문인데, 따라서 lvalue는 lvalue로, rvalue는 rvalue로 참조 타입을 유지시켜주는 std::forward가 등장했다.

template<typename T>
void func(T&& arg) {
    someOtherFunc(std::forward<T>(arg));
}

다음은 씹어먹는 C++의 예시이다.

#include <iostream>

template <typename T>
void wrapper(T&& u) {
  g(std::forward<T>(u));
}

class A {};

void g(A& a) { std::cout << "좌측값 레퍼런스 호출" << std::endl; }
void g(const A& a) { std::cout << "좌측값 상수 레퍼런스 호출" << std::endl; }
void g(A&& a) { std::cout << "우측값 레퍼런스 호출" << std::endl; }

int main() {
  A a;
  const A ca;

  std::cout << "원본 --------" << std::endl;
  g(a);
  g(ca);
  g(A());

  std::cout << "Wrapper -----" << std::endl;
  wrapper(a);
  wrapper(ca);
  wrapper(A());
}
원본 --------
좌측값 레퍼런스 호출
좌측값 상수 레퍼런스 호출
우측값 레퍼런스 호출
Wrapper -----
좌측값 레퍼런스 호출
좌측값 상수 레퍼런스 호출
우측값 레퍼런스 호출

Reference Collapsing rule은 레퍼런스 겹침 규칙으로, &의 개수의 기우성에 따라 참조의 타입을 결정하는 방식이다.

&가 홀수라면 A&(lvalue reference), 짝수라면 A&&(rvalue reference)가 된다.

Categories:

Updated:

Comments