C / C++,  Programming

[C++] Rvalue Reference

Rvalue reference에 대해 말하기 위해선 Rvalue, Lvalue에 대해서 알아야 한다.

Rvalue, Lvalue

Rvalue, lvalue라는 용어는 general한 용어로 = 기호를 기준으로 말하면 이해하기 쉽다.

Lvalue = Rvalue

Lvalue는 어떤 변수에 저장되어 지속되는 값을 의미하며, Rvalue는 이런 지속적인 값을 가지지 않는 임시의 값을 나타낸다.

Lvalue

#include <iostream>
int main() {
    int a = 777;
    int &b = a;
    std::cout << b << std::endl;
    return 0;
}

위 코드는 777이라는 값이 출력 될 것이다. int &b = a를 통해 a의 주소를 b가 가지고 있기 때문에 a의 값에 저장된게 b에 저장된 a 주소를 통해 접근 가능하다.

Rvalue

#include <iostream>
int main() {
    int a = 777;
    // int &lval = 10; // Error
    int &lval = a;
    // int &&rval = a; // Error
    int &&rval = 10;
    std::cout << lval << std::endl;
    std::cout << rval << std::endl;
    return 0;
}

위에 보면 int & 타입의 경우는 내부에 또 다른 주소값을 통해 참조하는 타입이며, rvalue는 int && 와 같이 연산자를 나타낼 수 있는데 이는 불필요한 복사를 제거하기 위해서 제안된 기술이다.

Rvalue Reference

#include <iostream>
#include <cstring>
#include <vector>

using namespace std;

class A
{
public:
    A (int _size): size(_size), data(new int[_size])
    {
        cout << "Normal constructor (" << _size << ")"<< endl;
    }
    A (const A& _rhs): size(_rhs.size), data(new int[_rhs.size])
    {
        cout << "Copy constructor (" << _rhs.size << ")"<< endl;
        memcpy(data, _rhs.data, _rhs.size);
    }
    A& operator=(const A& _rhs)
    {
        if (this != &_rhs)
        {
            cout << "Copy assignment operator (" << _rhs.size << ")" << endl;
            delete[] data;
            size = _rhs.size;
            data = new int[size];
            memcpy(data, _rhs.data, _rhs.size);
        }

        return *this;
    }
    ~A()
    {
        cout << "Destructor (" << size << ")" << endl;

        if (data != NULL) 
        {
            cout << "[Delete resources]" << endl;
            delete[] data;
        }
    }
private:
    int *data;
    int size;
};

int main() {
    vector<A> list;

    cout << "######### 1st ########" << endl;
    list.push_back(A(1));
    cout << "######### 2nd ########" << endl;
    list.push_back(A(2));
    cout << "######### 3rd ########" << endl;
    list[0] = A(3);
    cout << "######### Done ########" << endl;
    return 0;
}
########## 1st ##########
 Normal constructor (1)
 Copy constructor (1)
 Destructor (1)
 [Delete resources]
########## 2nd ##########
 Normal constructor (2)
 Copy constructor (2)
 Copy constructor (1)
 Destructor (1)
 [Delete resources]
 Destructor (2)
 [Delete resources]
########## 3rd ##########
 Normal constructor (3)
 Copy assignment operator (3)
 Destructor (3)
 [Delete resources]
########## Done ##########
 Destructor (3)
 [Delete resources]
 Destructor (2)
 [Delete resources]

위 코드를 보면 vector 내부에 object 2개를 push_back 함수를 통해 넣는다. vector 구조 특성상 capacity는 size의 2배를 유지해야하기 때문에 size가 capacity의 1/2보다 커지게 되면 새로운 메모리를 할당하는 동작이 이뤄진다. 이때 불필요하게 memory allocation & free 동작이 이뤄질 수 있는데 data size에 따라서 성능에 큰 영향을 미칠 수 있다.

위 결과를 보면 copy constructor가 수행될 때마다 새로운 메모리를 할당받고 destruction 된다. [Delete resources] 라는 message가 표기되는 것을 통해 매번 memory free 동작이 일어난다. 이런 새로운 memory allocation & free 을 단순한 move 변경 하기 위해서 move construction이 사용된다.

Memory allocation 시점

  1. A(1)
  2. list.push_back(A(1));
  3. A(2);
  4. list.push_back(A(2));
  5. A(3);
#include <iostream>
#include <cstring>
#include <vector>

using namespace std;

class A
{
public:
    A (int _size): size(_size), data(new int[_size])
    {
        cout << "Normal constructor (" << _size << ")"<< endl;
    }

    A (const A& _rhs): size(_rhs.size), data(new int[_rhs.size])
    {
        cout << "Copy constructor (" << _rhs.size << ")"<< endl;
        memcpy(data, _rhs.data, _rhs.size);
    }
    A (A&& _rhs): data(NULL), size(0)
    {
        cout << "Move constructor (" << _rhs.size << ")" << endl;
        data = _rhs.data;
        size = _rhs.size;

        _rhs.data = NULL;
    }
    A& operator=(const A& _rhs)
    {
        if (this != &_rhs)
        {
            cout << "Copy assignment operator (" << _rhs.size << ")" << endl;
            delete[] data;
            size = _rhs.size;
            data = new int[size];
            memcpy(data, _rhs.data, _rhs.size);
        }

        return *this;
    }
    A& operator=(A&& _rhs)
    {
        if (this != &_rhs)
        {
            cout << "Move assignment operator (" << _rhs.size << ")" << endl;
            delete[] data;

            size = _rhs.size;
            data = _rhs.data;

            _rhs.data = NULL;
        }

        return *this;
    }
    ~A()
    {
        cout << "Destructor (" << size << ")" << endl;

        if (data != NULL) 
        {
            cout << "[Delete resources]" << endl;
            delete[] data;
        }
    }
private:
    int *data;
    int size;
};

int main() {
    vector<A> list;
    cout << "######### 1st ########" << endl;
    list.push_back(A(1));
    cout << "######### 2nd ########" << endl;
    list.push_back(A(2));
    cout << "######### 3rd ########" << endl;
    list[0] = A(3);
    cout << "######### Done ########" << endl;
    return 0;
}
######### 1st #########
 Normal constructor (1)
 Move constructor (1)
 Destructor (0)
 ######### 2nd #########
 Normal constructor (2)
 Move constructor (2)
 Copy constructor (1)
 Destructor (1)
 [Delete resources]
 Destructor (0)
 ######### 3rd #########
 Normal constructor (3)
 Move assignment operator (3)
 Destructor (0)
 ######### Done #########
 Destructor (3)
 [Delete resources]
 Destructor (2)
 [Delete resources]

결과 log를 보면 실제 resource deallocation이 일어난 횟수는 1번 뿐이다. 그전엔 총 5번의 메모리 할당이 이뤄졌지만 move constructor & assignment를 통해 메모리 관리를 효율적으로 할 수 있었다.

코드상에서 큰 차이라고 한다면 A (A&& _rhs): data(NULL), size(0) move constructor & operator가 추가가 되며 내부에 memory allocation (new) & free (delete) 이 없이 단순하게 address 전달만 해준다.

참고로 C++11 version 이상에선 기본적인 타입의 vector에서 move semantics (rvalue reference)가 추가됐기 때문에 성능상 높은 version compiler를 쓰는게 유리하다.

Reference

  1. https://psychoria.tistory.com/54
  2. https://welikecse.tistory.com/1

Leave a Reply

Your email address will not be published. Required fields are marked *