Skip to content

move semantics and forward reference {C++}


std::move#

static_cast이다, 즉, 런타임에 무조건적으로 l -> r value로 캐스팅한다.

template <class T>
typename remove_reference<T>::type&&
move(T&& a)
{
    return a;
}

또는

static_cast<remove_reference<MyType>&&>(myValue);

non-default move ctor & move assignment#

모든 멤버들이 default move 연산을 제공한다면 "= default"를 붙이거나 아예 암묵적으로 만들어버릴 수도 있으나, 포인터같이 별도로 이동연산을 제공해야만 하는 경우 두개의 연산을 구현해야 한다.

Self(Self&& w), Self &operator=(Self &&w)

Move Constructor#

이때 주의해야 할 것이, 모든 멤버들또한 copy가 이루어지지 않도록 move를 수행해야 한다는 것이다. 왜냐면 인자로 들어온 w라는 놈은 l-value이기 때문.

class Widget {
private:
    int i{0};
    string s{};
    int *pi{nullptr};
public:
    // move ctor
    Widget( Widget && w ) noexcept
        // phase 1: Member wise move
        : i (std::move(w.i))
        , s (std::move(w.s))
        , pi(std::move(w.pi)) 
    {
        // phase 2: Reset
        w.pi = nullptr;
    }
};

위 코드에서 i와 pi는 primitive 타입이기 때문에 단순히 복사가 이루어지는 것이 최선이다. 하지만 convention을 위해 명시적으로 move 연산을 박아넣었다. 만약 저 pi라는 놈이 unique_ptr라면? default 연산으로 적용가능.

이동생성자 본문에 w.pi를 undefined state로 바꿔넣었지만, exchange(T& obj, U&& new_value) 를 사용하면 pi에는 w.pi를, w.pi에는 nullptr로 바꿔넣을 수 있다.

위의 코드는 the goal of move ctor 두개를 모두 지킨다.

  1. Transfer the content of w into this
  2. Leave w in a valid but undefined state

Core Guideline C.66: 이동생성자를 noexcept로 만들어라. 컴파일러 최적화 수행으로 60% 속도향상을 볼 수 있다. throw할 수 있는 연산 안에 이동생성자가 끼어있다면 컴파일러가 안정성을 보장하기 위해 copy를 할 수도 있다고.

Move Assignment Operator#

Widget &operator=(Widget && w) noexcept
{
    delete pi; // explicitly delete pi related memory

    i = std::move(w.i);
    s = std::move(w.s);
    pi = std::move(w.pi);

    w.pi = nullptr; // reset

    // 또는 아래와 같이 써도 된다.
    pi = std::exchange(w.pi, nullptr);

    return *this
}

move와는 직결되지 않지만 메모리 누수 버그를 없애기 위해 첫줄에 delete pi를 하는 모습을 볼 수 있다.

The goal of move assignment operator

  1. Clean up all visible resources
  2. Transfer the content of w into this
  3. Leave w in a valid but undefined state

Forwarding References 혹은 Uniform Reference#

  • reference collapsing
  • perfect forwarding
  • forwarding references (or uniform reference)
  • perils of forwarding references
  • pitfalls of r-value references
template<typename T>
void foo(T &&) {
    puts("foo(T&&)");
}

int main() {
    Widget w{};
    foo(w); // prints foo(T&&)
}

위의 코드가 정상동작하는 이유는 type deduction에 의해 T가 Widget&로 치환됐기 때문이고, 아래 reference collapsing에 의해 void foo(Widget& &&)void foo(Widget&)가 됐기 때문에 정상작동 하는 것이다.

reference-collapsing.png

forward reference는 l-value, r-value 모두를 받는다. 따라서 Uniform Reference라고도 부른다.

std::forward의 작동방식과 std::move의 차이점 분석#

perfect-forwarding.png

std::forward는 reference collapsing을 기반으로 작동한다. lvalue ⟶ lvalue reference를 반환하고, rvalue ⟶ rvalue reference를 반환한다.

이를 가능하게 하기 위해 forwarding reference를 사용하며, move는 이를 사용하지 않는다.

위의 영상은 forward를 사용하는 표준 함수인 make_unique의 구현부를 가져왔으며, args가 어떤 종류의 value이던 간에 다 받아 refernece로 전달한다.

rvalue reference와 모양이 흡사한데, 차이점이 무엇인가요?

rvalue reference는 말 그대로 rvalue reference만 받는 인자를 의미하지만, forwarding reference는 lvalue, rvalue 모두 받는다는 차이가 있습니다.

forwarding reference 사용시 주의사항#

정확한 타입을 지정하지 않으면 죄다 그 forwarding ref 함수로 빨려들어갈 수 있다. 예를 들어

struct Person {
    Person(const string& name); // (1)
    template <typename T> Person(T && name); // (2)
};

가 있을때, Person("Bjarne")는 2번이 호출된다. 왜냐면 문자열 리터럴은 const char *이거든. string name = "Herb"; Person(name) 또한 2번이 호출된다. 왜냐면 namestring이지, const string이 아니기 때문이다.

Move Semantics Pitfalls#

Q. 아래의 코드에 있는 문제를 식별하라.

class A {
public:
    template <typename T>
    A( T&& t )
        : b_(move(t))
    {}
private:
    B b_;
};
A(int{1});

A: forwarding reference에 의해 t는 lvalue-ref가 될 수도 있고, rvalue-ref가 될 수도 있다. 문제는 rvalue-ref일때인데, move는 인자로 lvalue를 받기 때문에 rvalue를 넣으면 터진다.

따라서, std::movestd::forward<T>로 바꿔줘야한다.

Q. 아래의 코드에 있는 문제를 식별하라.

template <typename T>
class A {
public:
    A( T&& t )
        : b_(std::forward<T>(t))
    {}
private:
    B b_;
};

템플릿 인스턴싱 이후에 A 생성자는 forwarding reference가 아닌, rvalue-reference를 인자로 받는다.

따라서, std::forwardstd::move로 바꿔줘야 한다.

Q. 아래의 코드에 있는 문제를 식별하라.

class A {
public:
    template <typename T>
    A( T&& t )
        : b_(std::forward<T>(t))
        , c_(std::forward<T>(t))
    {}
private:
    B b_;
    C c_
};

forward는 일종의 move연산이기 때문에 double move 문제가 발생한다. 따라서 첫번째 forward를 복사연산으로 바꾸던가 해야한다.

Q. 아래의 코드에 있는 문제를 식별하라.

class A {
public:
    template <typename T1, typename T2>
    A( T1&& t1, T2&& t2 )
        : b_(std::forward<T1>(t1))
        , c_(std::forward<T2>(t2))
    {}
private:
    B b_;
    C c_
};

🆗 개별적인 타입에 대한 forwarding reference를 진행하고 있기 때문에 문제없다.

Q. 아래의 코드에 있는 문제를 식별하라.

cppreference.com / Constexpr_if

template<typename T>
void foo(T&&)
{
    if constexpr(std::is_integral_v<T>) {
        // Deal with integral type
    } else {
        // Deal with non-integral type
    }
}

forwarding reference는 결국 어느걸 집어넣어도 레퍼런스가 튀어나온다는 것을 알고있다. 그런데 is_integral<int&>은 false이기에, if constexpr 가 성공할 수가 없다. 따라서 레퍼런스를 벗겨주어야 한다.

using NoRef = std::remove_reference<T>;
if constexpr(std::is_integral_v<NoRef>) {...} else {...}