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();
    }