배움 저장소

[홍정모의 따라하며 배우는 C++] 15. 의미론적 이동과 스마트 포인터 본문

Programming Language/C++

[홍정모의 따라하며 배우는 C++] 15. 의미론적 이동과 스마트 포인터

시옷지읏 2022. 1. 1. 13:20

15.1 이동의 의미와 스마트 포인터

Move Semantics and Smart Pointers


스마트 포인터를 이해하기 위하여 이동의 의미를 이해하자

 

RAII : Resource Acquisition Is Initialization, ... binds the life cycle of a resource to the lifetime of an object.

 

RAII를 위반하는 경우

- 동적할당 메모리를 저장하고 있는 포인터가 소멸될 때 동적할당메모리도 해제되어야 한다. 아래와 같은 경우 메모리를 해제하기 이전에 함수가 종료되어 메모리 누수가 발생하게 된다.

int *ptr = new int[10];
// work with dynamic mem
if (true) {
    return;
}
delete [] ptr;

if 조건문에 메모리 해제 코드를 추가하자

if (true) {
    delete [] ptr;
    return;
}

이렇게 중간에 프로그램이 종료되는 경우가 생기면 메모리 해제를 잊을 수 있다. Smart Pointer가 이를 해결해준다.

 

auto_ptr

- auto_ptr로 스마트 포인터 개념을 이해해보자. c+11부터 사용하지 않고, c++17부터 사라졌다. 

- Resource 클래스는 생성자와 소멸자가 호출될 때 각 문자열 출력으로 이를 알린다. 소멸자가 호출로 메모리 누수를 확인하자.

 

AutoPtr을 사용하여 동적 메모리를 할당하고, 메모리 해제를 자동으로 수행시켜 보자

- 갑작스럽게 프로그램이 종료되어도 메모리해제가 실행된다

class Resource {
public:
    int m_data[100];
    Resource(){ cout << "Resource created!" << endl; }
    ~Resource() { cout << "Resource destroyed!" << endl; }
};

template<class T>
class AutoPtr{
public:
    T *m_ptr;
    AutoPtr(T* ptr = nullptr):m_ptr(ptr){}
    ~AutoPtr(){
        if(m_ptr != nullptr) delete m_ptr;
    }

    T& operator*() const { return *m_ptr; }
    T* operator->() const { return m_ptr; }
    bool isNull() const { return m_ptr == nullptr; }
};
int main() {
    try {
        AutoPtr<Resource> autoptr(new Resource);
        if (true) { throw -1; }
        cout << "try is done!" << endl;
    }
    catch (...) {
        cerr << "Catching it!" << endl;
    }
}
Resource created!
Resource destroyed!
Catching it!

 

auto_ptr의 한계

- 대입 연산자를 활용하여 auto_ptr을 다른 인스턴스에 할당해보자. 영역이 끝이나면 소멸자가 각각 호출되는데 둘 다 동일한 동적할당 메모리를 해제하여 에러가 발생한다.

template<class T>
class AutoPtr{
public:
    T *m_ptr;
    AutoPtr(T* ptr = nullptr):m_ptr(ptr){}
    ~AutoPtr(){
        if(m_ptr != nullptr) delete m_ptr;
    }

    T& operator*() const { return *m_ptr; }
    T* operator->() const { return m_ptr; }
    bool isNull() const { return m_ptr == nullptr; }
};

int main() {
    AutoPtr<int> ptr1(new int);
    AutoPtr<int> ptr2;
    cout << ptr1.m_ptr << endl; // >> 0088DE20
    cout << ptr2.m_ptr << endl; // >> 00000000

    ptr2 = ptr1;
    cout << ptr1.m_ptr << endl; // >> 0088DE20
    cout << ptr2.m_ptr << endl; // >> 0088DE20
}

 

Move Semantics

- move semantics는 동적할당 메모리, 포인터 소유권을 넘긴다

- 복사된 인스턴스로 인해 메모리해제가 두 번 실행되었다. 동적할당 메모리의 소유권을 이전하면 문제가 해결된다.

- 소유권 이전이 포함된 복사생성자, 대입연산자를 구현하자. 두 인스턴스가 동적할당 메모리를 공유할 일이 사라진다.

AutoPtr(AutoPtr& a) {
    m_ptr = a.m_ptr;
    a.m_ptr = nullptr;
}
AutoPtr& operator = (AutoPtr& a) {
    if(&a == this){ return *this; }
    delete m_ptr;
    m_ptr = a.m_ptr;
    a.m_ptr = nullptr;
    return *this;
}

위를 구현하고 프로그램을 다시 실행시키면 에러가 사라진다.

template<class T>
class AutoPtr{
public:
    T *m_ptr;
    AutoPtr(T* ptr = nullptr):m_ptr(ptr){}
    ~AutoPtr(){
        if(m_ptr != nullptr) delete m_ptr;
    }
    AutoPtr(AutoPtr& a) {
        m_ptr = a.m_ptr;
        a.m_ptr = nullptr;
    }
    AutoPtr& operator = (AutoPtr& a) {
        if(&a == this){ return *this; }
        delete m_ptr;
        m_ptr = a.m_ptr;
        a.m_ptr = nullptr;
        return *this;
    }

    T& operator*() const { return *m_ptr; }
    T* operator->() const { return m_ptr; }
    bool isNull() const { return m_ptr == nullptr; }
};
int main() {
    AutoPtr<int> ptr1(new int);
    AutoPtr<int> ptr2;
    cout << ptr1.m_ptr << endl; // >> 0131D678
    cout << ptr2.m_ptr << endl; // >> 00000000

    ptr2 = ptr1; // Move semantics
    cout << ptr1.m_ptr << endl; // >> 00000000
    cout << ptr2.m_ptr << endl; // >> 0131D678
}

- 위에서 구현된 복사연산자와 대입연산자는 값을 복사하지 않는다.

- value semantics(copy semantics)가 아닌 move semantics를 사용하고 있다.

- semantics의 종류는 세 가지다. reference semantics(pointer)도 있다.

 

Syntax VS Semantics

- Syntax : 컴파일 OX를 따지는 문법

- Semantics : 프로그래머가 구현하려는 의도/의미

 

auto_ptr의 극복할 수 없는 한계

- 함수의 매개변수로 auto_ptr을 사용할 수 없다. auto_ptr의 소유권이 지역변수로 이동해버리면 auto_ptr의 동적할당 메모리는 사라져버린다. 프로그래머가 이를 막기 위하여 여럿 노력을 해야한다.

- 이러한 문제를 해결하기 위해 더 좋아진 스마트 포인터가 등장했고 auto_ptr은 사용하지 않게되었다.

 

15.2 오른쪽-값 참조

R-value References


Move Semantics(소유권 이동)을 사용하는 경우와 사용하지 않는 경우를 구분할 수 있는 오른쪽-값 참조를 알아보자

 

R-value와 L-value 복습

- x는 L-value로 값 5를 저장공간 x에 저장하고 있다. 이 때 값 5는 R-value로 임시로 사용되었다가 사라진다.

int x = 5;
const int c_x = 10;

 

L-value References

- 참조하고 있는 값이 주소를 가지고 있으면 L-value Reference를 초기화할 수 있다.

int &ref1 = x;   // OK, Modifiable L-value
int &ref2 = c_x; // Er! Non-modifiable L-value
int &ref3 = 5;   // Er! R-value

const int& c_ref1 = x;  // OK, Modifiable L-value
const int& c_ref2 = c_x;// OK, Non-modifiable L-value
const int& c_ref3 = 5;  // OK, R-value

 

R-value Reference

- R-value만 가질 수 있는 참조자이다. Ampersand(&)를 두개 사용한다. L-value reference와 구분하기 위함이다. 곧 사라질 값을 참조한다는 개념이 낯설다. R-value와 L-value를 구분하기 위해서 만들어졌다. R-value Reference에 담긴 임시 값은 해당 변수와 소멸을 함께한다.

int&& r_ref1 = x;   // Er! Modifiable L-value
int&& r_ref2 = c_x; // Er! Non-modifiable L-value
int&& r_ref3 = 5;   // OK, R-value

 

R-value와 L-value를 구분하는 함수 오버로딩

- R-value Reference 자료형 매개변수는 R-value만 받는다. 주소를 가지지 않는 임시값만 인자로 받을 수 있다.

void distinguish(int& l_ref) {
    cout << "L-value Reference" << endl;
}
void distinguish(int&& r_ref) {
    cout << "R-value Reference" << endl;
}

int main() {
    int x = 5;
    int &ref = x;   
    int&& r_ref = 5;

    distinguish(x);     // >> L-value Reference
    distinguish(ref);   // >> L-value Reference
    distinguish(r_ref); // >> L-value Reference
    distinguish(5);     // >> R-value Reference
}

 

R-value Reference를 활용하여 Move semantics 사용하기

- R-value Reference는 임시값이기 때문에 소유권을 이전해도 된다! L-value Reference로 받은 매개변수는 누군가 소유권을 가지고 있으므로 소유권을 가져올 경우 코드 밖에서 사용이 불가능하거나 에러를 발생시킨다.

- R-value ref와 L-value ref 오버로딩을 활용하여 move semantics를 활용할 수 있다.

 

15.3 이동 생성자와 이동 대입

Move constructors and Move assignment


Move Constructor(이동 생성자)는 복사 생성자와 달리 소유권을 이전해준다

- 이동생성자를 호출한 인스턴스에게 소유권을 넘긴다. 인자로 사용된 인스턴스의 소유권은 사라진다.

- Move semantics를 대입 연산자에서도 구현할 수 있다.

 

예제에 필요한 클래스이다. 

- Resource 클래스는 동적 메모리를 할당 받아 관리한다.

- AutoPtr 클래스는 Resource 인스턴스를 동적으로 할당받아 관리한다.

class Resource {
public:
    int *m_data = nullptr;
    unsigned m_length = 0;
public:
    Resource(unsigned length=0):m_length(length) { 
        cout << "Resource Constructor!" << endl; 
        m_data = new int [m_length];
    }
    Resource(const Resource& r): Resource(r.m_length) {
        cout << "Resource Deep Copy Constructor!" << endl;
        for (unsigned i = 0; i < m_length; ++i) {
            m_data[i] = r.m_data[i];
        }
    }
    ~Resource() { 
        cout << "Resource Destructor!" << endl; 
        if(m_data != nullptr) delete[] m_data;
     }

    Resource& operator = (Resource& r) {
        cout << "Resource Deep Copy assignmnet Operator!" << endl;
        if(this == &r) return *this;

        if(this->m_data != nullptr) delete [] m_data; // important to check n_ptr

        this->m_length = r.m_length;
        this->m_data = new int[this->m_length];

        for (unsigned i = 0; i < m_length; ++i)
            this->m_data[i] = r.m_data[i];

        return *this;
    }
    
    void print() {
        for(unsigned i=0; i<m_length; ++i)
            cout << m_data[i] << " ";
        cout << endl;
    }
};

class AutoPtr{
public:
    Resource* m_ptr;
    AutoPtr(Resource* ptr = nullptr) :m_ptr(ptr) {
        cout << "AutoPtr Constructor" << endl;
    }
    ~AutoPtr() {
        cout << "AutoPtr Destructor" << endl;
        if (m_ptr != nullptr) delete m_ptr;
    }
    AutoPtr(const AutoPtr& a) {// L-value ref
        cout << "AutoPtr Deep Copy Constructor" << endl;

        //deep copy, slow.
        m_ptr = new Resource;
        *m_ptr = *a.m_ptr;// addres is diff, val is same
                         // call class Resource's = operator 
    }

    AutoPtr& operator = (const AutoPtr& a) {
        cout << "AutoPtr Copy assignmnet Operator" << endl;

        if (&a == this) { return *this; }
        if(this->m_ptr != nullptr) delete m_ptr;
        m_ptr = new Resource;
        *m_ptr = *a.m_ptr;// addres is diff, val is same
                         // call class Resource's = operator 
        return *this;
    }
};

 

generateResource( ) 함수를 실행하여 어떤 생성자가 호출되는지 확인하자

AutoPtr generateResource() {
    AutoPtr resource(new Resource(10000000));
    return resource; // copied and return
}

int main() {
    AutoPtr test_resource;
    test_resource = generateResource();
}

주의) release 모드와 debug 모드가 다르게 작동한다

debug 모드:

- 인스턴스를 반환할 때 복사 생성자를 호출하고 원본은 소멸자를 호출한다.

- 복사 생성된 인스턴스는 R-value return값으로 사용된다. 영역 밖에서 값을 할당해주고 소멸한다.

release모드:

- 최적화 된 코드를 실행한다. 복사 생성자를 생략하고 내부 값을 영역 밖 변수에 값을 할당하고 난 뒤에 소멸한다.

1. Debug Mode                             ||  2. Release Mode
AutoPtr Constructor                       ||  AutoPtr Constructor
Resource Constructor!                     ||  Resource Constructor!
AutoPtr Constructor                       ||  AutoPtr Constructor
AutoPtr Deep Copy Constructor             ||  AutoPtr Copy assignmnet Operator
Resource Constructor!                     ||  Resource Constructor!
Resource Deep Copy assignmnet Operator!   ||  Resource Deep Copy assignmnet Operator!
AutoPtr Destructor                        ||  AutoPtr Destructor
Resource Destructor!                      ||  Resource Destructor!
AutoPtr Copy assignmnet Operator          ||
Resource Constructor!                     ||
Resource Deep Copy assignmnet Operator!   ||
AutoPtr Destructor                        ||  
Resource Destructor!                      ||  
AutoPtr Destructor                        ||  AutoPtr Destructor
Resource Destructor!                      ||  Resource Destructor!

Timer 클래스를 추가하여 복사 생성자가 얼마나 비효율적인지 계산해보자. 동적할당 메모리의 모든 값을 복사생성자로 옮기면 속도는 느리다. Timer.h: 10.5 의존 관계 - [홍정모의 따라하며 배우는 C++] 10.객체들 사이의 관계에 대해

 

참고) cout 출력은 프로그램 실행 속도에 큰 영향을 준다. 다음 코드를 입력하여 cout 출력을 비활성화 할 수 있다

streambuf* orig_buf = cout.rdbuf();
cout.rdbuf(NULL); // disconnect cout from buffer stop cout

cout << "cout didn't work!! in here" << endl;

cout.rdbuf(orig_buf); // restore cout
cout << "cout work as expected" << endl;

 

테스트 코드

- 아래 코드를 실행하면 0.084 ~ 0.088의 시간이 소요된다. 대입 생성자 대신 이동 생성자를 사용해보자

int main() {
    streambuf* orig_buf = cout.rdbuf();
    cout.rdbuf(NULL); // disconnect cout from buffer stop cout

    Timer timer;
    {
        AutoPtr test_resource;
        test_resource = generateResourece();
    }
    cout.rdbuf(orig_buf);
    timer.elapse();
    cout << "cout work as expected" << endl;
}

 

AutoPtr 클래스에 이동 생성자와 이동 대입연산자를 구현해보자

- 이동 생성자와 이동 대입연산자는 값을 복사하지 않고 소유권만 이전한다.

AutoPtr(AutoPtr&& a):m_ptr(a.m_ptr) { // R-value ref
    cout << "AutoPtr Move Constructor" << endl;
        
    a.m_ptr = nullptr; // Move functions should always leave 
}                     // both objects in a well-defined state

AutoPtr& operator = (AutoPtr&& a) { // R-value ref
    cout << "AutoPtr Move assignmnet Operator" << endl;

    if (&a == this) { return *this; } // prevent self assignment
    if(this->m_ptr != nullptr) delete m_ptr;

    // shallow copy
    this->m_ptr = a.m_ptr;
    a.m_ptr = nullptr; // Move functions should always leave 
                      // both objects in a well-defined state
    return *this;
}

테스트 코드를 다시 실행시키면 이동연산자와 이동 생성자가 실행됨을 알 수 있다. 속도는 훨씬 빨라졌다.

0.0093~0.0100이다. 복사 생성자를 사용했을 때는 0.084~0.088이다. Move Semantic을 사용하면 9배이상 빨라진다.

1. Debug Mode                    ||  2. Release Mode
AutoPtr Constructor              ||  AutoPtr Constructor
Resource Constructor!            ||  Resource Constructor!
AutoPtr Constructor              ||  AutoPtr Constructor
AutoPtr Move Constructor         ||  
AutoPtr Destructor               ||  
AutoPtr Move assignmnet Operator ||  AutoPtr Move assignmnet Operator
AutoPtr Destructor               ||  AutoPtr Destructor
AutoPtr Destructor               ||  AutoPtr Destructor
Resource Destructor!             ||  Resource Destructor!

 

15.4 std::move


std::move( )함수는 매개변수를 R-value로 반환한다. 이를 활용하여 복사생성자 대신 이동생성자를 호출 할 수 있다.

 

Resource 클래스는 깊은 복사 생성자와 깊은 복사 대입연산자가 구현되었다

Autoptr 클래스는 깊은 복사 생성자, 깊은 복사 대입연산자, 이동생성자, 이동 대입 연산자가 구현되었다

class Resource {
public:
    int *m_data = nullptr;
    unsigned m_length = 0;
public:
    Resource(unsigned length=0):m_length(length) { 
        cout << "Resource Constructor!" << endl; 
        m_data = new int [m_length];
    }
    Resource(const Resource& r): Resource(r.m_length) {
        cout << "Resource Deep Copy Constructor!" << endl;
        for (unsigned i = 0; i < m_length; ++i)
            m_data[i] = r.m_data[i];
    }
    ~Resource() { 
        cout << "Resource Destructor!" << endl; 
        if(m_data != nullptr) delete[] m_data;
     }

    Resource& operator = (Resource& r) {
        cout << "Resource Deep Copy assignmnet Operator!" << endl;
        if(this == &r) return *this;

        if(this->m_data != nullptr) delete [] m_data; // important to check n_ptr

        this->m_length = r.m_length;
        this->m_data = new int[this->m_length];

        for (unsigned i = 0; i < m_length; ++i)
            this->m_data[i] = r.m_data[i];

        return *this;
    }
    
    void print() {
        for(unsigned i=0; i<m_length; ++i)
            cout << m_data[i] << " ";
        cout << endl;
    }
};

class AutoPtr{
public:
    Resource* m_ptr;
    AutoPtr(Resource* ptr = nullptr) :m_ptr(ptr) {
        cout << "AutoPtr Constructor" << endl;
    }
    ~AutoPtr() {
        cout << "AutoPtr Destructor" << endl;
        if (m_ptr != nullptr) delete m_ptr;
    }

    AutoPtr(const AutoPtr& a) {// L-value ref
        cout << "AutoPtr Deep Copy Constructor" << endl;

        //deep copy, slow.
        m_ptr = new Resource;
        *m_ptr = *a.m_ptr;// addres is diff, val is same
                         // call class Resource's = operator 
    }

    AutoPtr& operator = (const AutoPtr& a) {
        cout << "AutoPtr Copy assignmnet Operator" << endl;

        if (&a == this) { return *this; }
        if(this->m_ptr != nullptr) delete m_ptr;
        m_ptr = new Resource;
        
        //deep copy
        *m_ptr = *a.m_ptr;// addres is diff, val is same
                         // call class Resource's = operator 
        return *this;
    }
    
    //AutoPtr(const AutoPtr& a) = delete;
    //AutoPtr& operator = (const AutoPtr& a) = delete;

    AutoPtr(AutoPtr&& a):m_ptr(a.m_ptr) { // R-value ref
        cout << "AutoPtr Move Constructor" << endl;
        
        m_length = a.m_length;
        a.m_ptr = nullptr; // Not necessary, buf safe
    }

    AutoPtr& operator = (AutoPtr&& a) { // R-value ref
        cout << "AutoPtr Move assignmnet Operator" << endl;

        if (&a == this) { return *this; } // prevent self assignment
        if(this->m_ptr != nullptr) delete m_ptr;

        // shallow copy
        this->m_ptr = a.m_ptr;
        a.m_ptr = nullptr; // Not necessary, but safe

        return *this;
    }
    Resource* operator ->() const { return this->m_ptr; }
    Resource& operator *() const { return *(this->m_ptr); }
};

아래의 코드를 실행하면 복사생성자가 호출된다.

int main() {
    AutoPtr r1(new Resource(10000000));
    cout << r1.m_ptr << endl;

    AutoPtr r2 = r1;
    
    cout << r1.m_ptr << endl;
    cout << r2.m_ptr << endl;
}
Resource Constructor!
AutoPtr Constructor
005BDD40
AutoPtr Deep Copy Constructor
Resource Constructor!
Resource Deep Copy assignmnet Operator!
005BDD40
005BDC28
...

이동 생성자와 복사 생성자 모두 구현되었을 때 대입연산자는 복사생성자를 호출한다

std::move( )를 활용하여 이동생성자를 호출할 수 있다 

- std::move( )는 매개변수를 r-value로 반환한다. R-value ref를 매개변수로 받은 이동 연산자를 호출한다.

AutoPtr r2 = std::move(r1);
Resource Constructor!
AutoPtr Constructor
00E8DF10
AutoPtr Move Constructor
00000000
00E8DF10
...

이동연산자가 호출된다. 소유권이 이전되고 기존의 인스턴스는 소유권을 잃는다.

 

std::move( )를 활용하여 swap 구현하기

- swap( ) 함수는 std::move( ) 함수를 활용하기 좋은 예제이다. 원본이 소유권을 잃어도 임시 값이기 때문에 상관없다. deep-copy보다 효율적으로 사용할 수 있다.

 

Resource 클래스에 멤버함수를 추가하였다. 이 함수는 멤버 배열의 모든 값을 특정값으로 만든다. 

CopySwap( )함수와 MoveSwap( ) 함수를 구현하였다

void Resource::setAll(const int& v) {
    for(unsigned i=0; i<m_length; ++i)
        m_data[i] = v;
}

void CopySwap(AutoPtr& left, AutoPtr& right) {
    AutoPtr temp = left; // Copy constructor
    left = right;        // copy assignment
    right = temp;        // copy assignment
}

void MoveSwap(AutoPtr& left, AutoPtr& right) {
    AutoPtr temp(std::move(left)); // move constructor
    left = std::move(right);       // r-value assignment oper
    right = std::move(temp);       // r-value assignment oper
}

int main() {
    AutoPtr r1(new Resource(3));
    r1->setAll(3);
    AutoPtr r2(new Resource(5));
    r2->setAll(5);
    
    r1->print();
    r2->print();
    
    // CopySwap(r1,r2);
    MoveSwap(r1,r2);

    r1->print();
    r2->print();
}

MoveSwap( )함수를 보자. std::move( )을 사용하여 이동생성자와 이동 대입연산자를 호출했다.

...
3 3 3
5 5 5 5 5
AutoPtr Move Constructor
AutoPtr Move assignmnet Operator
AutoPtr Move assignmnet Operator
AutoPtr Destructor
5 5 5 5 5
3 3 3
...

move 함수에서 사용된 인자는 소유권을 잃을 수 있다. 이를 주의하자.

 

Standard library에서 std::move( ) 활용하기

- AutoPtr 내부에서 R-value 복사생성자와 R-value 대입연산자가 구현되었기 때문에 move( )를 사용했을 때 결과 값이 달라졌다. standard library에도 R-value 복사생성자와 R-value 대입연산자가 구현되어있기 때문에 move( )를 활용할 수 있다. string은 내부에 R-value를 위한 멤버함수가 구현되어 있어 move( )를 활용할 수 있다

vector<string> v;
string lvalue = "Hello";

/* Call function take L-value reference */
v.push_back(lvalue);

cout << lvalue << endl;      // >> Hello
cout << v[0] << endl;        // >> Hello

/* Call function take R-value reference*/
string rvalue_test = "World";        // loose 
v.push_back(std::move(rvalue_test)); // Move semantics

cout << rvalue_test << endl; // >> 
cout << v[1] << endl;        // >> World

 

std::move( )를 활용하여 swap함수를 구현해보자

template<typename T>
void CopySwap(T& left, T& right) {
    T temp = left; // Copy constructor
    left = right;  // copy assignment
    right = temp;  // copy assignment
}
template<typename T>
void MoveSwap(T& left, T& right) {
    T temp(std::move(left)); // move constructor
    left = std::move(right);       // r-value assignment oper
    right = std::move(temp);       // r-value assignment oper
}
int main() {
    string l = "Hello";
    string r = "World";

    cout << l << " " << r << endl;
    MoveSwap<string>(l, r);
    cout << l << " " << r << endl;
}

 

15.5 std::unique_ptr


데이터의 소유권이 단일 인스턴스인 경우에 unique_ptr을 사용한다.

- <memory.h>를 포함하여 사용할 수 있다. unique_ptr 인스턴스가 소멸될 때 동적할당 메모리를 해제해준다.

unique_ptr<int> u_iptr(new int(4));
unique_ptr<string> u_sptr(new string("Unique Pointer!"));

cout << *u_iptr << endl; // >> 4
cout << *u_sptr << endl; // >> Unique Pointer!

생성자를 직접 호출하기 보다 make_unique를 사용하자

- make_unqiue는  exception이 잘 구현되어있다.

- 코드가 간결해진다.

- new keyword를 사용하지 않아도 된다.

auto ai = std::make_unique<int>(4);
// unique_ptr<int>
auto as = std::make_unique<string>("Unique Pointer");
// unique_ptr<string>

cout << *ai << endl; // >> 4
cout << *as << endl; // >> Unique Pointer

 

unique_ptr은 대입연산자로 값을 복사할 수 없다

- unique_ptr에 담긴 정보는 다른 인스턴스와 공유할 수 없다. 문법적으로 금지되어있다. 

unique_ptr<string> u_ptr1(new string("Hello"));
unique_ptr<string> u_ptr2;
u_ptr1 = u_ptr2; // Error! operator= is a deleted function

 

unique_ptr은 move를 활용하여 R-value reference 대입연산자를 사용할 수 있다

-  소유권을 잃은 unique_ptr은 nullptr이다. nullptr는 00000000이므로 false가 출력된다

unique_ptr<string> u_ptr1(new string("Hello"));
unique_ptr<string> u_ptr2;

cout << u_ptr1 << endl; // >> 00B58DE8
cout << u_ptr2 << endl; // >> 00000000

cout << boolalpha;
cout << "u_ptr1:" << static_cast<bool>(u_ptr1) << endl; // >> u_ptr1:true
cout << "u_ptr2:" << static_cast<bool>(u_ptr2) << endl; // >> u_ptr2:false

// u_ptr1 = u_ptr2;  Error!
u_ptr2 = move(u_ptr1);
cout << "u_ptr1:" << static_cast<bool>(u_ptr1) << endl; // >> u_ptr1:false
cout << "u_ptr2:" << static_cast<bool>(u_ptr2) << endl; // >> u_ptr2:true

 

unique_ptr의 멤버함수 오버로딩

- 포인터처럼 -> 연산자와 *연산자 모두 사용가능하다. 멤버함수로 오버로딩되어 있다.

unique_ptr<string> u_ptr1(new string("Hello"));

u_ptr1->push_back('A');
cout << *u_ptr1 << endl; // >> HelloA

 

unique_ptr을 함수 매개변수로 사용하는 경우

- 아래 코드는 에러를 발생시킨다.

- unique_ptr은 특정 데이터를 소유할 수 있는 단일 인스턴스로 복사생성자를 호출할 수 없도록 지워버렸다.

void ArguTypeTest(unique_ptr<string> u_ptr){}

int main() {
    unique_ptr<string> u_ptr(new string("Hello"));
    ArguTypeTest(u_ptr); //Error! copied constructor is deleted! 
}

아래와 같이 소유권을 이전시키면 호출가능하다. 소유권을 받은 지역변수는 소멸되고 원본은 nullptr이 된다.

void ArguTypeTest(unique_ptr<string> u_ptr){}

int main() {
    unique_ptr<string> u_ptr(new string("Hello"));
    cout << u_ptr << " " << *u_ptr << endl; // >> 01174ED0 Hello

    ArguTypeTest(move(u_ptr)); // OK

    cout << u_ptr << " " << *u_ptr << endl; // >> 00000000
}

 

 

소유권 되돌려받기

- 소유권을 가져간 지역변수에게서 소유권을 다시 뺏어오자. ArguTypeTest의 매개변수 local_uptr를 unique_ptr로 반환해주자. 함수 외부에서 소유권을 가져갈 수 있다.

unique_ptr<string> ArguTypeTest(unique_ptr<string> local_uptr){
    return local_uptr;
}

int main() {
    unique_ptr<string> u_ptr(new string("Hello"));
    cout << u_ptr << " " << *u_ptr << endl; // >> 01174ED0 Hello

    u_ptr = ArguTypeTest(move(u_ptr)); // Move Semantics

    cout << u_ptr << " " << *u_ptr << endl; // >> 01174ED0 Hello
}

L-value reference를 매개변수로 사용하면 간단하다.

void ArguTypeTest(unique_ptr<string> &local_uptr){
    // do sth..
}

 

unique_ptr가 저장하고 있는 값을 매개변수로 사용하고 싶다면 어떻게 해야할까?

- 멤버함수 get( )을 호출해보자. 데이터의 주소를 반환한다.

void ArguTypeTest(string* s) {
    // do sth
}

int main() {
    unique_ptr<string> u_ptr(new string("Hello"));

    ArguTypeTest(u_ptr.get());
    cout << *u_ptr.get() << endl; // >> Hello
    cout << *u_ptr << endl;       // >> Hello
}

 

unique_ptr을 사용할 때 주의하기

(1) 같은 데이터를 공유하는 unique_ptr는 만들지말자

- 아래 코드를 실행하면 중복하여 메모리를 해제한다. 프로그램이 중단되고 하단의 cout은 출력되지 않는다.

{
    string* s_ptr = new string("Hello");

    unique_ptr<string> u_ptr1(s_ptr);
    unique_ptr<string> u_ptr2(s_ptr);
}
cout << "Program is work without any Problem" << endl;

 

(2) unique_ptr 이외의 주체가 메모리를 해제하면 unique_ptr 소멸자에서 중복하여 메모리를 해제한다

{
    string* s_ptr = new string("Hello");

    unique_ptr<string> u_ptr(s_ptr);
    cout << *u_ptr << endl;
    delete s_ptr;
}
cout << "Program is work without any Problem" << endl;

 

(3) 임시 값을 함수 인자로 사용할 때 unique_ptr보다 make_unique를 사용하자

void ArguTypeTest(unique_ptr<string> u_ptr) {
    cout << &u_ptr << endl;
    cout << u_ptr << endl;
    cout << *u_ptr << endl;
}

int main() {
    ArguTypeTest(unique_ptr<string>(new string("Hello")));// Bad
    ArguTypeTest(make_unique<string>(string("Hello")));   // Good
    ArguTypeTest(make_unique<string>("Hello"));      // very Good
}

참고) https://stackoverflow.com/questions/53870522/why-use-stdmake-unique-in-c17

 

15.6 std::shared_ptr


소유권을 여러 인스턴스가 공유할 수 있는 shared_ptr을 사용해보자

- shared_ptr은 특정 데이터를 몇 개의 인스턴스와 공유하고 있는지 기록한다.

- 다른 인스턴스가 해당 데이터를 가지고 있지 않을 때에만 메모리를 해제한다.

class Resource {
public:
    int *m_data = nullptr;
    unsigned m_length = 0;
public:
    Resource(unsigned length=0):m_length(length) { 
        cout << "Resource Constructor!" << endl; 
        m_data = new int [m_length];
    }
    ~Resource() { 
        cout << "Resource Destructor!" << endl; 
        if(m_data != nullptr) delete[] m_data;
     }
};

int main() {
    Resource* r = new Resource(5);
    {
        shared_ptr<Resource> s_ptr1(r);
        {
            shared_ptr<Resource> s_ptr2(s_ptr1);
            cout << &s_ptr1 << " " << s_ptr1 << endl;
            cout << &s_ptr2 << " " << s_ptr2 << endl;
        }
        cout << "Out of inner block!" << endl;
    }
    cout << "Out of outer block!" << endl;
}

share_ptr은 같은 정보를 공유할 수 있다. 아래 s_ptr1과 s_ptr2가 담고 있는 값이 동일함을 확인하자.

>> Resource Constructor!
>> 00D3F758 010BE010
>> 00D3F748 010BE010
>> Out of inner block!
>> Resource Destructor!
>> Out of outer block!

 

share_ptr의 잘못된 사용

- 새로운 인스턴스를 만들 때 기존의 인스턴스를 참조해야 한다. 아래와 같이 데이터로 바로 초기화하면 인스턴스의 개수를 셀 수 없어 메모리 해제를 두 번 하게된다.

Resource* r = new Resource(5);
shared_ptr<Resource> s_ptr1(r);
shared_ptr<Resource> s_ptr2(r);

 

{
    Resource* r = new Resource(5);
    {
        shared_ptr<Resource> s_ptr1(r);
        {
            shared_ptr<Resource> s_ptr2(r);
            cout << &s_ptr1 << " " << s_ptr1 << endl;
            cout << &s_ptr2 << " " << s_ptr2 << endl;
        }
        cout << "Out of inner block!" << endl;
    }
    cout << "Out of outer block!" << endl;
}
cout << "execute code without problem" << endl;

에러가 발생한다. 마지막 execute code without problem이 출력되지 않는다.

Resource Constructor!
00D3F9C4 00D5DF10
00D3F9B4 00D5DF10
Resource Destructor!
Out of inner block!
Resource Destructor!

 

실수를 줄이는 make_shared

- auto keyword와 함께사용하자. 코드가 간결해진다

auto good_ptr1 = std::make_shared<string>("Hello");
auto good_ptr2 = good_ptr1;

shared_ptr<string> s_ptr1(new string("Hello"));
shared_ptr<string> s_ptr2(s_ptr1);

 

15.7 순환 의존성 문제와 std::weak_ptr

Circular dependency issues and


shared_ptr의 순환 의존성 문제를 알아보자.

 

순환참조를 막아줄 수 있는 스마트 포인터 weak_ptr를 알아보자.

- shared_ptr은 해당 데이터를 공유하는 인스턴스 개수를 세고있다. weak_ptr은 그렇지 않아 순환참조를 막아줄 수 있다.

 

순환 의존성 문제를 보여주기 위한 클래스이다

class Person {
public:
    string m_name;
    shared_ptr<Person> m_partner;

    Person(const string& name) : m_name(name) {
        cout << m_name << " is created" << endl;
    }
    ~Person() {
        cout << m_name << " is deleted" << endl;
    }
    friend bool PartnerUp(shared_ptr<Person> &left, shared_ptr<Person> &right) {
        if(!left || !right) return false;

        left->m_partner = right;
        right->m_partner = left;
        cout << left->m_name << " " << right->m_name << " is partner\n";
        return true;
    }
};

 

Circular Dependency(순환 의존성)

- 아래 코드를 실행하면 소멸자가 호출되지 않는다. 서로의 멤버변수로 초기화되어 shared_ptr의 데이터를 공유하고 있는 인스턴스가 2개가 되었다.

- 인스턴스가 1개가 될 수 없어 소멸자는 호출되지 앟는다. 이를 순환 의존성 문제라 한다.

- 순환 의존성 문제는 메모리 누수를 발생시킨다.

int main() {
    auto dwayne = make_shared<Person>("Dwayne");
    auto vin = make_shared<Person>("Vin");
    cout << dwayne.use_count() << endl; // >> 1
    cout << vin.use_count() << endl;    // >> 1

    PartnerUp(dwayne, vin);
    cout << dwayne.use_count() << endl; // >> 2
    cout << vin.use_count() << endl;    // >> 2
}

 

weak_ptr사용해보기

- 클래스 멤버변수 unique_ptr을 weak_ptr으로 바꾸어주자.

- 변경 이후 위 코드를 실행하면 소멸자가 호출되고 메모리가 해제된다.

//shared_ptr<Person> m_partner; // change this
weak_ptr<Person> m_partner;    // to this

 

문제점: weak_ptr는 내부에 저장된 멤버에 접근할 수 없다

int main() {
    auto dwayne = make_shared<Person>("Dwayne");
    auto vin = make_shared<Person>("Vin");

    PartnerUp(dwayne, vin);

    cout << dwayne->m_partner->m_name;// Error!
    cout << vin->m_partner->m_name;   // Error!
}

 

weak_ptr에 저장된 데이터를 사용하려면 shared_ptr로 변환시켜주어야 한다

- weak_ptr 클래스의 멤버함수 .lock( )을 사용하자. shared_ptr로 변환시킨다.

const shared_ptr<Person> Person::getWeakPtrData() {
    return m_partner.lock();
}

위를 활용하면 에러없이 실행가능하다.

int main() {
    auto dwayne = make_shared<Person>("Dwayne");
    auto vin = make_shared<Person>("Vin");
   
   PartnerUp(dwayne, vin);

   cout << dwayne->getWeakData() << endl;
   cout << vin->getWeakData() << endl;
   //cout << dwayne->m_partner.lock() << endl;
   //cout << vin->m_partner.lock() << endl;
}

 

Comments