c++ 11에서는 move semantics와 관련된 std::move와 std::forward가 추가되었습니다. 

 두 함수를 설명드리기 전에 기본적인 개념인 보편 참조와 참조 축약에 대해서 설명드립니다. 

 해당 개념을 이해하셔야 std::move와 std::forward의 내부 동작 방식을 이해하실 수 있습니다. 

rvalue reference vs universal reference(보편 참조)

 c++에서 참조는 &와 &&으로 표현됩니다. &는 lvalue 참조를 의미합니다.

 &&은 문맥에 따라서 rvalue 참조 또는 universal(보편) 참조를 의미합니다. (마치 한글의 동음이의어와 같습니다.)

 rvalue 참조는 쉽게 이해가 되지만 universal(보편) 참조가 의미하는 내용은 무엇일까요?

 universal(보편) 참조는 rvalue 참조 또는 lvalue 참조 둘 다 될 수 있다는 것을 의미합니다. 

 문맥에 따라서 유동적으로 변환 될 수 있는 참조입니다. 

// rvalue 참조
void f(Person&& obj) 

// rvalue 참조
Person&& p1 = Person("ahn", 1985); 

// universal 참조
// var1는 auto로 명시적이지 않습니다.
auto && var1 = var2		

// rvalue 참조
// param은 std::vector로 명시적이고 vector 내부 객체가 T만 연역
template<typename T>
void tf(std::vector<T>&& param);

// universal 참조
// param은 T로 명시적이지 않습니다.
template<typename T>
void tf2(T&& param);

 위의 예제를 보면 문맥에 따라서 rvalue 참조인지 universal(보편) 참조인지 구분되는 것을 확인 할 수 있습니다.

 위의 예제를 보면 알 수 있듯이 대상 자체가 명시적이지 않으며 형식 연역이 발생하면

 universal(보편) 참조가 되는 것을 확인 할 수 있습니다. 

 일반적으로 rvalue 참조에는 std::move를 사용하고 universal(보편) 참조 std::forward를 통해서 전달합니다.

참조 축약

 참조 축약이란 universal 참조에서 참조의 참조의 형태의 데이터형이 만들어 질 수 있는데

 이 때 컴파일러에 의해서 참조의 참조를 lvalue 참조나 rvalue 참조로 변환하는 것을 말합니다.

template<typename T>
void func(T&& param)    // param은 보편 참조

Person p1("ahn", 1985);

func(p1);                    // lvalue 호출
// fun(p1)은 아래와 같은 형식으로 연역됩니다.
// T = Person&
=> void func(Person& && param) => void func(Person & param)


func(std::move(p1))          // rvalue 호출
// fun(std::move(p1))은 아래와 같은 형식으로 연역됩니다.
// T = Person
=> void func(Person && param)

 위의 예제를 보면 보편 참조에 Person lvalue 참조를 전달하면 T는 Person &으로 연역됩니다. 

 파라미터 타입은 Person & && param의 형식이 됩니다. 실제로는 참조의 참조는 존재하지 않기 때문에 컴파일러는 

 Person & && param => Person & param로 변환합니다. 

 보편 참조에 Person rvalue 참조를 전달하면 T는 Person으로 연역되서 파라미터는 Person && param로 지정됩니다.

참조 축약 변환 첫번째 참조 & 첫번째 참조 &&
두번째 참조 & lvalue 참조 lvalue 참조
두번째 참조 && lvalue 참조 rvalue 참조

 컴파일러의 참조 축약의 변환 규칙은 참조의 참조중에 하나라도 lvalue 참조라면 lvalue 참조로 변환 합니다.

 그렇지 않다면 rvalue 참조로 변환 합니다.

예제로 사용할 Person 클래스

using namespace std;


struct Person
{
    string name;
    int* year = nullptr;

    Person()
    {}

    Person(string p_name, const int p_year) : name(move(p_name)), year(new int(p_year))
    {
        cout << "constructed" << endl;
    }

    Person(const Person& other) noexcept :
        name(move(other.name)), year(new int(*other.year))
    {
        cout << "copy constructed" << endl;
    }

    Person(Person&& other) noexcept :
        name(move(other.name)), year(nullptr) 
    {
        // Rvalue의 힙에서 할당된 year를 이동시킵니다.
        year = other.year;
        // Rvalue의 year를 nullptr 초기화 시킵니다.
        other.year = nullptr;

        cout << "move constructed" << endl;
    }

    Person& operator=(const Person& other) noexcept
    {
        if (this != &other)
        {
            this->name = other.name;
            this->year = new int(*other.year);
        }
        cout << "copy Assignment operator" << endl;
        return *this;
    }
    
    Person& operator=(Person&& other) noexcept
    {
        if (this != &other)
        {
            this->name = std::move(other.name);

            if (this->year) delete this->year;

            this->year = other.year;
            other.year = nullptr;
        }
        cout << "move Assignment operator" << endl;
        return *this;
    }
    
    virtual ~Person()
    {
        if (nullptr != year)
        {
            delete year;
        }
        cout << "destructed " << endl;
    }
};

std::move

 std::move는 전달된 파라미터를 강제로 rvalue 참조로 캐스팅하여 리턴하는 함수입니다. 

 내부를 살펴보면 간단히 구현되어 있습니다. 캐스팅만 수행하고 어떠한 작업도 하지 않습니다. 

template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;

// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
    return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

 예제를 통해서 std::move 사용법을 알아 봅니다.

    Person p1("ahn", 1985);
    // p1을 std::move를 통해서 이동연산이 가능하도록 캐스팅합니다.
    Person p2= std::move(p1);

이동연산이 수행후 메모리 구조
결과 값

 예제 코드를 보면 p1이라는 lvalue를 std::move를 통해서 rvalue로 변환 후 이동 생성자를 통해서 p2를 생성합니다. 

 std::move를 보면 모든 값을 rvalue로 캐스팅 할 수 있을 것 같지만 rvalue로 변환될 수 없는 대상도 있습니다. 

    const Person p1("ahn", 1985);
    
    // const Person인 p1을 std::move를 통해서 캐스팅합니다. 
    // 하지만 const Person&&으로 변환되고 const가 붙어서 이동 생성자가 채택되지 못하고
    // p2는 복사 생성자로 생성됩니다.
    Person p2= std::move(p1);

결과 값

 p1의 데이터형을 const Person으로 변환해서 std::move를 실행하면 다른 결과를 확인 할 수 있습니다. 

 p1은 std::move를 통해서 const Person &&로 캐스팅 됩니다. Person 내부에서 생성자를 선택할 때 const 때문에

 이동 생성자가 채택되지 못하고 복사 생성자를 채택되고 p2는 복사생성자를 통해서 생성됩니다.

 이것은 코드를 작성자의 의도와는 다르게 동작할 가능성이 높습니다.(std::move를 사용한다는건 이동 연산 선호)

 이동을 수행 할 객체는 const로 선언하지 말아야 합니다.

std::move의 내부동작 

// _Arg가 lvalue 참조인 경우
template <class Person &>
_NODISCARD constexpr remove_reference_t<Person &>&& move(Person & && _Arg) noexcept {
    return static_cast<remove_reference_t<Person &>&&>(_Arg);
}

// _Arg가 rvalue 참조인 경우
template <class Person>
_NODISCARD constexpr remove_reference_t<Person>&& move(Person&& _Arg) noexcept { 
    return static_cast<remove_reference_t<Person>&&>(_Arg);
}

// 두 형식 모두 아래와 같이 해석됩니다. 
_NODISCARD constexpr Person && move(Person&& _Arg) noexcept { 
    return static_cast<Person &&>(_Arg);
}

 내부적으로는 위와 같이 호출됩니다. 보편 참조이기 때문에 lvalue나 rvalue로 연역될 수 있는 데

 remove_reference_t를 사용하여 _Ty타입의 참조을 제거 하여 두가지 연역 모두 동일한 형식으로 변환 되도록 

 구현 되어 있는 것을 확인 할 수 있습니다.

std::forward

 이 함수도 std::move처럼 캐스팅만 수행합니다. std::move와는 다르게 조건에 따라서 다른 값을 리턴합니다. 

 입력 값이  lvalue 참조가 아니라면 rvalue 참조(이동연산지원)를 리턴하고

 lvalue 참조라면 수정하지 않고 그대로 리턴합니다. 

// FUNCTION TEMPLATE forward
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept 
{ // forward an lvalue as either an lvalue or an rvalue
    return static_cast<_Ty&&>(_Arg);
}

template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept 
{ // forward an rvalue as an rvalue
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);
}

 std::forward가 가장 많이 쓰이는 곳은 보편 참조 매개변수로 받아서 처리하는 로직입니다. 

// lvalue를 캐치하는 함수
void Catch(Person& p, const char* name)
{
    cout << name << "lvalue catch" << endl;
}

// rvalue를 캐치하는 함수
void Catch(Person&& p, const char * name)
{
    cout << name << "rvalue catch" << endl;
}

// 전달받은 obj를 std::forward를 통해서 catch 함수로 전달합니다.
template<typename T>
void ForwardingObj(T&& obj, const char* name)
{
    Catch(std::forward<T>(obj), name);
}

int _tmain(int argc, _TCHAR* argv[])
{
    Person p1("ahn", 1985);
    ForwardingObj(p1, "p1\t\t=\t");
    ForwardingObj(std::move(p1), "std::move(p1)\t=\t");

    return 0;
}

수행결과

 예제 코드를 보면 ForwardingObj라는 함수는 보편 참조인 obj를 받아서 Catch함수를 수행합니다. 

 std::forward을 통해서 obj의 파라미터 입력값이  lvalue 참조면 수정 없이 Catch를 수행하고 rvalue 참조면 rvalue

 참조로 캐스팅해서 이동연산을 수행합니다. 

 여기서 주의 할 점은 rvalue 참조면 그냥 넘겨도 동작 해야 하지 않냐는 것이다.

 이것이 move semantics를 처음 접할 때 가장 혼란스러운 부분인데 잘 기억 해야 합니다. 

 매개변수가 rvalue 참조인 경우에도 매개변수 자체는 lvalue입니다. 매개변수는 항상 lvalue입니다.

 왜냐하면 매개변수는 주소를 가질 수 있습니다. 

 그럼 rvalue 참조를 전달하기 위한 방법이 필요한데 그 방법으로 std::forward 함수를 사용합니다.

 매개변수의 값이 rvalue 참조일 경우 이동 연산이 지원되는 rvalue 참조로 캐스팅하는 역활을 합니다. 

std::forward의 내부동작 

template<typename T>
void func(T&& param)    // param은 보편 참조
{
    return std::forward<T>(param);
}

/////////////////////////////////////////////////////////////////////////////

// param이 lvalue Person이라면 
template <class Person &>
_NODISCARD constexpr Person & && forward(
    remove_reference_t<Person &>& _Arg) noexcept { 
    return static_cast<Person & &&>(_Arg);
}
// 참조 축약으로 인해서 아래와 같이 변경
// lvalue 참조로 캐스팅해서 전달
template <class Person &>
_NODISCARD constexpr Person & forward(
    remove_reference_t<Person &>& _Arg) noexcept { 
    return static_cast<Person &>(_Arg);
}

//////////////////////////////////////////////////////////////////////////////

// param이 rvalue Person이라면
// rvalue 참조로 캐스팅해서 전달
template <class Person>
_NODISCARD constexpr Person && forward(
    remove_reference_t<Person>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<Person&&>(_Arg);
}

 참조 축약을 통해서 forward 호출 구문을 변경해보면 위와 같이 변형되서 호출 되기 때문에 std::forward를 통해서 

 조건부 캐스팅이 가능해 집니다.

'c++ > modern c++' 카테고리의 다른 글

[c++] constexpr 키워드  (0) 2020.03.19
[c++] auto와 decltype 키워드  (0) 2020.03.15
[c++] Move semantics  (0) 2020.03.07
[c++] 람다(Lambda) 표현식  (0) 2020.03.04
[c++] weak_ptr  (1) 2020.03.02

+ Recent posts