std::shared_ptr — это интеллектуальный указатель, который сохраняет совместное владение объектом через указатель. Объект удаляется, когда последний оставшийся общий указатель на него выходит за пределы области действия.

Эта реализация предназначена для демонстрации основных функций std::shared_ptr, но не является полной реализацией и не предназначена для использования в производственном коде.

template<typename T>
class shared_ptr {
  template <typename U> 
  friend class shared_ptr;
public:
  explicit shared_ptr(T* ptr = nullptr) : ptr_(ptr), count_(new std::atomic<std::size_t>(1)) { }

  ~shared_ptr() {
    if (--*count_ == 0) {
      delete ptr_;
      delete count_;
    }
  }

  shared_ptr(const shared_ptr& other) : ptr_(other.ptr_), count_(other.count_) {
    ++*count_;
  }
  
  // generalized copy constructor
  template <typename U>
  shared_ptr(const shared_ptr<U>& other) : ptr_(other.ptr_), count_(other.count_) {
    ++*count_;
  }

  shared_ptr& operator=(const shared_ptr& other) {
    shared_ptr(other).swap(*this);
    return *this;
  }

  shared_ptr(shared_ptr&& other) noexcept : ptr_(other.ptr_), count_(other.count_) {
    other.ptr_ = nullptr;
    other.count_ = new std::atomic<std::size_t>(1);
  }

  shared_ptr& operator=(shared_ptr&& other) noexcept {
    shared_ptr(std::move(other)).swap(*this);
    return *this;
  }

  void swap(shared_ptr& other) noexcept {
    using std::swap;
    swap(ptr_, other.ptr_);
    swap(count_, other.count_);
  }

  void reset(T* ptr) noexcept {
    shared_ptr(ptr).swap(*this);
  }

  T& operator*() const { return *ptr_; }
  T* operator->() const { return ptr_; }
  std::size_t use_count() const { return ptr_ == nullptr? 0: count_->load(); }
  explicit operator bool() const { return ptr_ != nullptr; }

private:
  T* ptr_;
  std::atomic<std::size_t>* count_;
};

Вот некоторые основные моменты:

  1. Счетчик ссылок увеличивается, когда новый экземпляр shared_ptr создается из существующего путем создания копии или назначения копии (правая сторона). Счетчик ссылок уменьшается, когда экземпляр shared_ptr выходит за пределы области действия, назначается копирование (слева) или когда вызывается функция reset. Счетчик ссылок используется для отслеживания того, сколько экземпляров shared_ptr совместно владеют одним и тем же динамически выделяемым объектом. Когда счетчик ссылок достигает нуля, это означает, что больше нет экземпляров shared_ptr, которые совместно владеют объектом, поэтому его можно безопасно удалить.
  2. Метод копирования и замены используется для реализации оператора копирования-присваивания. Идея этого метода заключается в определении временного экземпляра shared_ptr и обмене его содержимым с целевым экземпляром shared_ptr. Этот подход гарантирует, что целевой экземпляр shared_ptr останется в допустимом состоянии, даже если присваивание завершится ошибкой из-за возникновения исключения. Временный экземпляр shared_ptr уничтожается в конце области действия, а счетчик ссылок динамически выделяемого объекта корректно управляется. Точно так же метод reset использует ту же технику. И оператор присваивания перемещения использует технику перемещения и замены.
  3. Использование атомарного значения для счетчика ссылок должно сделать его потокобезопасным. В многопоточной среде несколько потоков могут одновременно обращаться к счетчику ссылок и изменять его, а атомарный тип обеспечивает необходимую синхронизацию, гарантирующую согласованное обновление счетчика ссылок между потоками.
  4. Обобщенный конструктор копирования позволяет создавать shared_ptr экземпляров из shared_ptr экземпляров разных, но связанных типов. Это полезно при работе с отношениями наследования между типами. Например, если класс Derived является производным от класса Base, можно создать экземпляр shared_ptr, указывающий на объект типа Derived, а затем использовать обобщенный конструктор копирования для создания нового экземпляра shared_ptr, который разделяет владение с исходным. shared_ptr, но имеет другой тип, связанный с исходным типом. Таким образом, shared_ptr можно использовать для управления временем жизни объекта производного типа, сохраняя при этом возможность использовать связь между производным типом и базовым типом. Обратите внимание, что объявление класса друга в начале объявления класса необходимо, чтобы заставить его работать. В противном случае other.ptr_ недоступен, поскольку shared_ptr<U> — это другой класс, а ptr_ — закрытый.

Вот несколько тестов для проверки функциональности shared_ptr:

struct Foo {
  Foo() { std::cout << "Foo created\n"; }
  ~Foo() { std::cout << "Foo destroyed\n"; }
};

struct Base {
  virtual void bar() { std::cout << "Base\n"; }
};

struct Derived: public Base {
  void bar() override { std::cout << "Derived\n"; }
};

int main() {
  // Test 1: Construct shared_ptr and verify the object is created
  {
    shared_ptr<Foo> p1(new Foo); // Foo created 
    std::cout << "Use count: " << p1.use_count() << '\n'; // outputs 1
  } // Foo destroyed

  // Test 2: Copy construct shared_ptr and verify use count is updated
  {
    shared_ptr<Foo> p1(new Foo); // Foo created
    shared_ptr<Foo> p2(p1); 
    std::cout << "Use count: " << p1.use_count() << '\n'; // outputs 2
  } // Foo destroyed

  // Test 3: Copy assignment shared_ptr and verify use count is updated
  {
    shared_ptr<Foo> p1(new Foo); // Foo created 
    shared_ptr<Foo> p2(new Foo); // Foo created
    p1 = p2;  // Foo destroyed
    std::cout << "Use count: " << p1.use_count() << '\n'; // outputs 2
    std::cout << "Use count: " << p2.use_count() << '\n'; // outputs 2
  } // Foo destroyed

  // Test 4: Reset shared_ptr and verify the original object is destroyed
  {
    shared_ptr<Foo> p1(new Foo); // Foo created 
    p1.reset(nullptr); // Foo destroyed 
  } 

  // Test 5: Move shared_ptr and verify the moved-from instance has a nullptr
  {
    shared_ptr<Foo> p1(new Foo); // Foo created 
    shared_ptr<Foo> p2 = std::move(p1);
    std::cout << "Use count: " << p2.use_count() << '\n'; // outputs 1
    std::cout << "p1 is null: " << (p1 ? "false" : "true") << '\n'; // outputs true
  } // Foo destroyed 

  // Test 6: Move assignment and verify the moved-from instance has a nullptr
  {
    shared_ptr<Foo> p1(new Foo); // Foo created 
    shared_ptr<Foo> p2;
    p2 = std::move(p1);
    std::cout << "Use count: " << p2.use_count() << '\n'; // outputs 1
    std::cout << "p1 is null: " << (p1 ? "false" : "true") << '\n'; // outputs true
  } // Foo destroyed 

  // Test 7: Generalized copy constructor with related types 
  {
    shared_ptr<Foo> p1(new Foo);  // Foo created 
    shared_ptr<Foo> p2(p1); 
    shared_ptr<const Foo> p3(p2);
    std::cout << "Use count: " << p3.use_count() << '\n'; // outputs 3
  } // Foo destroyed 


  // Test 8: Generalized copy constructor with related types
  {
    shared_ptr<Derived> p1(new Derived); 
    shared_ptr<Base> p2(p1);
    p2->bar(); // outputs Derived
  }

  return 0;
}

Эти тесты проверяют основные функции, такие как создание shared_ptr, копирование shared_ptr, сброс shared_ptr, перемещение shared_ptr и использование обобщенного конструктора копирования для создания shared_ptr родственного типа.

Связанное чтение

  1. Реализуйте свой собственный std::unique_ptr | Джаспер Чжун | февраль 2023 г. | Гений разработчиков
  2. Улучшите свои навыки управления памятью на C++: краткое введение в boost::intrusive_ptr | Джаспер Чжун | февраль 2023 г. | Гений разработчиков