C++/SmartPointer 2021. 3. 24. 10:48

순환 의존성 문제 (Circular Dependency Issues)

순환 의존성

  • shared_ptr가 클래스 내부에서 계속 존재하게 되면, 인스턴스가 소멸자를 호출하지 못한다.

  • 만약 인스턴스 두 개가 멤버로 서로를 shared_ptr로 가지고 있다면, 마치 교착 상태처럼 소멸자를 둘 다 호출하지 못하는 상태가 된다.

    #include <iostream>
    #include <memory>
    
    class Person
    {
      std::string                name_;
      std::shared_ptr<Person>    partner_;
    
    public:
      Person(const std::string& name) : name_(name)
      {
        std::cout << name_ << " created\n";
      }
    
      ~Person()
      {
        std::cout << name_ << " destroyed\n";
      }
    
      friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
      {
        if (!p1 || !p2)
          return false;
    
        p1->partner_ = p2;
        p2->partner_ = p1;
    
        std::cout << p1->name_ << " is partnered with " << p2->name_ << '\n';
    
        return true;
      }
    
      const std::string& getName() const
      {
        return name_;
      }
    };
    
    int            main()
    {
      auto lucy = std::make_shared<Person>("Lucy");
      auto ricky = std::make_shared<Person>("Ricky");
    
      partnerUp(lucy, ricky);
    }
    
    /* stdout stderr
    Lucy created
    Ricky created
    Lucy is partnered with Ricky
    */
    • 소멸자가 호출되지 않는 것을 볼 수 있다.

    • 마지막 줄의 partnerUp(lucy, ricky);를 지우면 정상적으로 소멸자가 호출된다.

    • weak_ptr로 해결할 수 있다.

C++/SmartPointer 2021. 3. 24. 10:44

이동 생성자와 이동 대입 (Move Constructor and Move Assignment)

속도 비교

  • L-value 레퍼런스와 R-value 레퍼런스의 성능 차이가 꽤 존재한다.

    • R-value 레퍼런스의 경우 Deep Copy를 하지 않기 때문이다.

    • 생성자, 소멸자를 호출하는 부분이 강의와 살짝 달랐는데, 강의는 Release 모드여서 Debug 모드에 비해 단계가 줄어있는 상태여서 그랬다.

      • 나도 x86버전의 Release모드로 컴파일했더니 실행 파일이 없다는 LNK1104 에러가 발생하였다.

      • 백신 문제였는데, x64로 컴파일하니 실행에 문제는 없었다. 이유는 모르겠다.

      • 아래는 Debug 모드로 실행했다.

  • L-value 레퍼런스를 사용한 예제

    • Copy Constructor, Copy Assignment를 이용해 값을 Deep Copy한다.

    main.cpp

    #include <iostream>
    #include "Resource.h"
    #include "AutoPtr.h"
    #include "Timer.h"
    
    using namespace std;
    
    AutoPtr<Resource> generateResource()
    {
      AutoPtr<Resource> res(new Resource(10000000));
    
      return res;
    }
    
    int            main()
    {
      Timer timer;
      {
        AutoPtr<Resource> main_res;
        main_res = generateResource();
      }
      timer.elapsed();
    }
    
    /* stdout stderr
    AutoPtr default constructor
    Resource length constructed
    AutoPtr default constructor
    AutoPtr copy constructor
    Resource default constructed
    Resource copy assignment
    AutoPtr destructor
    Resource destoryed
    AutoPtr copy assignment
    Resource default constructed
    Resource copy assignment
    AutoPtr destructor
    Resource destoryed
    AutoPtr destructor
    Resource destoryed
    0.0771911
    */

    Resource.h

    #pragma once
    
    #include <iostream>
    
    class Resource
    {
    public:
      int    *data_ = nullptr;
      unsigned length_ = 0;
    
      Resource()
      {
        std::cout << "Resource default constructed\n";
      }
    
      Resource(unsigned length)
      {
        std::cout << "Resource length constructed\n";
        init(length);
      }
    
      Resource(const Resource& res)
      {
        std::cout << "Resource copy constructed\n";
        init(res.length_);
        for (unsigned i = 0; i < length_; ++i)
          data_[i] = res.data_[i];
      }
    
      ~Resource()
      {
        std::cout << "Resource destoryed\n";
    
        if (data_ != nullptr) delete[] data_;
      }
    
      void    init(unsigned length)
      {
        data_ = new int[length];
        length_ = length;
      }
    
      Resource& operator = (Resource& res)
      {
        std::cout << "Resource copy assignment\n";
        if (&res == this) return *this;
    
        if (data_ != nullptr) delete[] data_;
        init(res.length_);
        for (unsigned i = 0; i < length_; ++i)
          data_[i] = res.data_[i];
        return *this;
      }
    
      void print()
      {
        for (unsigned i = 0; i < length_; ++i)
          std::cout << data_[i] << ' ';
        std::cout << std::endl;
      }
    };

    AutoPtr.h

    #pragma once
    
    #include <iostream>
    
    template<class T>
    class AutoPtr
    {
    public:
      T* ptr_;
    
      AutoPtr(T *ptr = nullptr)
        : ptr_(ptr)
      {
        std::cout << "AutoPtr default constructor\n";
      }
    
      AutoPtr(const AutoPtr& a)
      {
        std::cout << "AutoPtr copy constructor\n";
        ptr_ = new T;
        *ptr_ = *a.ptr_;
      }
    
      ~AutoPtr()
      {
        std::cout << "AutoPtr destructor\n";
        if (ptr_ != nullptr) delete ptr_;
      }
    
      AutoPtr& operator = (const AutoPtr& a)
      {
        std::cout << "AutoPtr copy assignment\n";
        if (&a == this)
          return *this;
    
        if (ptr_ != nullptr) delete ptr_;
    
        ptr_ = new T;
        *ptr_ = *a.ptr_;
        return *this;
      }
    };

    Timer.h

    #pragma once
    
    #include <iostream>
    #include <chrono>
    
    class Timer
    {
        using clock_t = std::chrono::high_resolution_clock;
        using second_t = std::chrono::duration<double, std::ratio<1>>;
    
        std::chrono::time_point<clock_t> start_time = clock_t::now();
    
    public:
        void elapsed()
        {
            std::chrono::time_point<clock_t> end_time = clock_t::now();
    
            std::cout << std::chrono::duration_cast<second_t>(end_time - start_time).count() << std::endl;
        }
    };

  • R-value 레퍼런스를 사용한 예제

    • AutoPtr 클래스에서 Move Constructor, Move Assignment를 이용했다.

    main.cpp

    #include <iostream>
    #include "Resource.h"
    #include "AutoPtr.h"
    #include "Timer.h"
    
    using namespace std;
    
    AutoPtr<Resource> generateResource()
    {
      AutoPtr<Resource> res(new Resource(10000000));
    
      return res;
    }
    
    int            main()
    {
      Timer timer;
      {
        AutoPtr<Resource> main_res;
        main_res = generateResource();
      }
    
      timer.elapsed();
    }
    
    /* stdout stderr
    AutoPtr default constructor
    Resource length constructed
    AutoPtr default constructor
    AutoPtr move constructor
    AutoPtr destructor
    AutoPtr move assignment
    AutoPtr destructor
    AutoPtr destructor
    Resource destoryed
    0.0090543
    */

    Resource.h

    #pragma once
    
    #include <iostream>
    
    class Resource
    {
    public:
      int    *data_ = nullptr;
      unsigned length_ = 0;
    
      Resource()
      {
        std::cout << "Resource default constructed\n";
      }
    
      Resource(unsigned length)
      {
        std::cout << "Resource length constructed\n";
        init(length);
      }
    
      Resource(const Resource& res)
      {
        std::cout << "Resource copy constructed\n";
        init(res.length_);
        for (unsigned i = 0; i < length_; ++i)
          data_[i] = res.data_[i];
      }
    
      ~Resource()
      {
        std::cout << "Resource destoryed\n";
    
        if (data_ != nullptr) delete[] data_;
      }
    
      void    init(unsigned length)
      {
        data_ = new int[length];
        length_ = length;
      }
    
      Resource& operator = (Resource& res)
      {
        std::cout << "Resource copy assignment\n";
        if (&res == this) return *this;
    
        if (data_ != nullptr) delete[] data_;
        init(res.length_);
        for (unsigned i = 0; i < length_; ++i)
          data_[i] = res.data_[i];
        return *this;
      }
    
      void print()
      {
        for (unsigned i = 0; i < length_; ++i)
          std::cout << data_[i] << ' ';
        std::cout << std::endl;
      }
    };

    AutoPtr.h

    #pragma once
    
    #include <iostream>
    
    template<class T>
    class AutoPtr
    {
    public:
      T* ptr_;
    
      AutoPtr(T *ptr = nullptr)
        : ptr_(ptr)
      {
        std::cout << "AutoPtr default constructor\n";
      }
    
      AutoPtr(AutoPtr&& a)
        : ptr_(a.ptr_)
      {
        std::cout << "AutoPtr move constructor\n";
        a.ptr_ = nullptr;
      }
    
      ~AutoPtr()
      {
        std::cout << "AutoPtr destructor\n";
        if (ptr_ != nullptr) delete ptr_;
      }
    
      AutoPtr& operator = (AutoPtr&& a)
      {
        std::cout << "AutoPtr move assignment\n";
        if (&a == this)
          return *this;
    
        if (ptr_ != nullptr) delete ptr_;
    
        ptr_ = a.ptr_;
        a.ptr_ = nullptr;
        return *this;
      }
    };

    Timer.h

    #pragma once
    
    #include <iostream>
    #include <chrono>
    
    class Timer
    {
        using clock_t = std::chrono::high_resolution_clock;
        using second_t = std::chrono::duration<double, std::ratio<1>>;
    
        std::chrono::time_point<clock_t> start_time = clock_t::now();
    
    public:
        void elapsed()
        {
            std::chrono::time_point<clock_t> end_time = clock_t::now();
    
            std::cout << std::chrono::duration_cast<second_t>(end_time - start_time).count() << std::endl;
        }
    };
  • 보다 정확한 시간을 측정하려면 출력 하지 않은 채로 시간을 재면 된다.

'C++ > SmartPointer' 카테고리의 다른 글

C++ 순환 의존성 문제 (Circular Dependency Issues)  (0) 2021.03.24
C++ R-value Reference  (0) 2021.03.24
C++ Syntax vs Semantics  (0) 2021.03.24
C++ 스마트 포인터 (Smart Pointer)  (0) 2021.03.22
C++/SmartPointer 2021. 3. 24. 10:35

R-value Reference

R-value

  • L-value와 달리 메모리 주소가 저장되지 않는 값을 의미한다.

예제

  • 주소를 가지고 있지 않은 리터럴 값이나 함수의 반환 값 등을 참조할 수 있다.

    #include <iostream>
    
    using namespace std;
    
    void        doSomething(int& ref)
    {
      cout << "L-value ref\n";
    }
    
    void        doSomething(int&& ref)
    {
      cout << "R-value ref\n";
    }
    
    int            getResult()
    {
      return 100 * 100;
    }
    
    int            main()
    {
      int x = 5;
      int y = getResult();
      const int cx = 6;
      const int cy = getResult();
    
      // L-value References
    
      int& lr1 = x;
      //int& lr2 = cx;
      //int& lr3 = 5;
    
      const int& lr4 = x;
      const int& lr5 = cx;
      const int& lr6 = 5;
    
    
// R-value references

//int&& rr1 = x;
//int&& rr2 = cx;
int&& rr3 = 5;
int&& rrr = getResult();

cout << rr3 << endl;
rr3 = 10;
cout << rr3 << endl;

//const int&& rr4 = x;
//const int&& rr5 = cx;
const int&& rr6 = 5;

doSomething(x);
doSomething(5);
doSomething(getResult());

}

/* stdout stderr
5
10
L-value ref
R-value ref
R-value ref
*/
```

C++/SmartPointer 2021. 3. 24. 10:31

Syntax vs Semantics

  • 아래의 코드는 문법(syntax) 상 문제가 없으나, 정수 + 정수문자열 + 문자열의 의미(semantics)가 다르다.

      int x = 1, y = 1;
      x + y;
    
      std::string str1("Hello"), str2(" World");
      str1 + str2;

Syntax

  • 문법에 잘 맞아서 컴파일이 되는지

Semantics

  • 의미가 무엇인지

  • Value Semantics (Copy Semantics)

  • Reference Semantics

  • Move Semantics

C++/SmartPointer 2021. 3. 22. 00:15

스마트 포인터 (Smart Pointer)

RAII (Resource Acquisition Is Initialization)

  • C++의 창시자인 스트롭스트룹이 제안한 디자인 패턴이다.

    • C++ RAII(Resource Acquisition Is Initialization)의 해석을 요약하면 다음과 같다.

      • 객체의 유효성에 대해 다음의 관점이 필요하다.

        • 생성이 완료된 객체는 유효한 객체이어야 한다. 즉, 유효하지 않은 객체가 생성되어서는 안 된다.

        • 필요한 자원들을 획득하지 못한 객체는 유효한 객체가 아니다.

      • 대칭성에 의해, 소멸되어서 더 이상 유효하지 않게 된 객체는 어떤 자원도 가지고 있지 않아야 한다.

  • 메모리 누수를 방지하는 기법이다.

    • 동적으로 할당한 메모리, 혹은 파일을 open한 뒤 early return 혹은 throw 등 예기치 못하게 스택을 되감는 경우에 메모리 누수가 발생할 수 있다.

      main.cpp

      #include <iostream>
      #include "Resource.h"
      
      using namespace std;
      
      void        doSomething()
      {
        Resource* res = new Resource;
      
        if (true)
          return ;
      
        delete res;
      }
      
      int            main()
      {
        doSomething();
      }
      
      /* stdout stderr
      Resource constructed
      */

      Resource.h

      #pragma once
      
      #include <iostream>
      
      class Resource
      {
      public:
        int    data_[100];
      
        Resource()
        {
          std::cout << "Resource constructed\n";
        }
      
        ~Resource()
        {
          std::cout << "Resource destoryed\n";
        }
      };

auto_ptr

  • C++98부터 auto_ptr 클래스가 존재했는데, 앞으로는 사용하지 않아야 되는 클래스이다.

    • 예기치 못한 상황이 발생할 수 있기 때문에, 더 안정적인 unique_ptr 등을 사용하면 된다.
  • 스택에 할당된 인스턴스가 자동으로 소멸되는 것을 이용했다.

  • 아래는 auto_ptr를 비슷하게 구현해보는 예제이다.

    main.cpp

    #include <iostream>
    #include "Resource.h"
    #include "AutoPtr.h"
    
    using namespace std;
    
    void        doSomething()
    {
      AutoPtr<Resource> res(new Resource);
    }
    
    int            main()
    {
      doSomething();
    }
    
    /* stdout stderr
    Resource constructed
    Resource destoryed
    */

    Resource.h

    #pragma once
    
    #include <iostream>
    
    class Resource
    {
    public:
      int    data_[100];
    
      Resource()
      {
        std::cout << "Resource constructed\n";
      }
    
      ~Resource()
      {
        std::cout << "Resource destoryed\n";
      }
    };

    AutoPtr.h

    #pragma once
    
    #include <iostream>
    
    template<class T>
    class AutoPtr
    {
    public:
      T* ptr_ = nullptr;
    
      AutoPtr(T *ptr = nullptr)
        : ptr_(ptr)
      {}
    
      ~AutoPtr()
      {
        if (ptr_ != nullptr) delete ptr_;
      }
    
      T& operator*() const { return *ptr_; }
      T* operator->() const { return ptr_; }
      bool isNull() const { return ptr_ == nullptr; }
    };

Move Semantics

  • 다음과 같은 경우 double free 문제가 발생하여 런타임 에러를 만날 수 있다.

    main.cpp

    #include <iostream>
    #include "Resource.h"
    #include "AutoPtr.h"
    
    using namespace std;
    
    void        doSomething()
    {
      AutoPtr<Resource> res1(new Resource);
      AutoPtr<Resource> res2;
    
      cout << boolalpha;
      cout << res1.ptr_ << endl;
      cout << res2.ptr_ << endl;
    
      res2 = res1;
    
      cout << res1.ptr_ << endl;
      cout << res2.ptr_ << endl;
    }  // 여기서 소멸자가 호출될 때 메모리를 두 번 해제하게 되어 에러 발생
    
    int            main()
    {
      doSomething();
    }

    Resource.h

    #pragma once
    
    #include <iostream>
    
    class Resource
    {
    public:
      int    data_[100];
    
      Resource()
      {
        std::cout << "Resource constructed\n";
      }
    
      ~Resource()
      {
        std::cout << "Resource destoryed\n";
      }
    };

    AutoPtr.h

    #pragma once
    
    #include <iostream>
    
    template<class T>
    class AutoPtr
    {
    public:
      T* ptr_ = nullptr;
    
      AutoPtr(T *ptr = nullptr)
        : ptr_(ptr)
      {}
    
      ~AutoPtr()
      {
        if (ptr_ != nullptr) delete ptr_;
      }
    
      T& operator*() const { return *ptr_; }
      T* operator->() const { return ptr_; }
      bool isNull() const { return ptr_ == nullptr; }
    };

  • 이런 상황을 방지하려면 copy construnctorassignment operator를 오버로딩해야 한다.

    • AutoPtr 클래스가 복사 생성자를 호출하거나 대입 연산을 할 때, 내부의 포인터가 옮겨가는 방식으로 구현한다.

    • 이렇게 되면 같은 포인터를 한 인스턴스만 갖도록 하여 메모리 해제의 중복 문제를 피할 수 있다.

    main.cpp

    #include <iostream>
    #include "Resource.h"
    #include "AutoPtr.h"
    
    using namespace std;
    
    void        doSomething()
    {
      AutoPtr<Resource> res1(new Resource);
      AutoPtr<Resource> res2;
    
      cout << boolalpha;
      cout << res1.ptr_ << endl;
      cout << res2.ptr_ << endl;
    
      res2 = res1;  // move semantics
    
      cout << res1.ptr_ << endl;
      cout << res2.ptr_ << endl;
    }
    
    int            main()
    {
      doSomething();
    }
    
    /* stdout stderr
    Resource constructed
    00E8E518
    00000000
    00000000
    00E8E518
    Resource destoryed
    */

    Resource.h

    #pragma once
    
    #include <iostream>
    
    class Resource
    {
    public:
      int    data_[100];
    
      Resource()
      {
        std::cout << "Resource constructed\n";
      }
    
      ~Resource()
      {
        std::cout << "Resource destoryed\n";
      }
    };

    AutoPtr.h

    #pragma once
    
    #include <iostream>
    
    template<class T>
    class AutoPtr
    {
    public:
      T* ptr_ = nullptr;
    
      AutoPtr(T *ptr = nullptr)
        : ptr_(ptr)
      {}
    
      AutoPtr(T& a)
      {
        ptr_ = a.ptr_;
        a.ptr_ = nullptr;
      }
    
      ~AutoPtr()
      {
        if (ptr_ != nullptr) delete ptr_;
      }
    
      AutoPtr& operator = (AutoPtr& a)
      {
        if (&a == this)
          return *this;
    
        delete ptr_;
        ptr_ = a.ptr_;
        a.ptr_ = nullptr;
        return *this;
      }
    
      T& operator*() const { return *ptr_; }
      T* operator->() const { return ptr_; }
      bool isNull() const { return ptr_ == nullptr; }
    };

Syntax vs Semantics


AutoPtr의 한계

  • 다음과 같이 함수 안으로 move semantics가 적용되는 경우, 해당 함수가 끝나면서 메모리가 해제된다.

    • 이후 해당 인스턴스가 또 소멸되는 상황이므로 런타임 에러가 발생한다.
    #include <iostream>
    #include "Resource.h"
    #include "AutoPtr.h"
    
    using namespace std;
    
    void        doSomething2(AutoPtr<Resource> res)
    {
    
    }
    
    void        doSomething()
    {
      AutoPtr<Resource> res1(new Resource);
    
      doSomething2(res1);
    }  // 에러
    
    int            main()
    {
      doSomething();
    }