Я попытался написать свой собственный класс shared_ptr, простой, чтобы лучше понять его, вместе с конструктором перемещения и копирования и оператором присваивания перемещения и копирования. Это выглядит примерно так: -
// Online C++ compiler to run C++ program online
#include <iostream>
#include <stdio.h>
template<class T>
class my_shared_ptr
{
private:
T* ptr = nullptr;
int* refcount = nullptr;
public:
my_shared_ptr() : ptr(nullptr),refcount(new int (0))
{std::cout <<"Inside my_shared_ptr constructor \n" ;}
my_shared_ptr(T* pT) : ptr(pT),refcount(new int (0))
{std::cout <<"Inside my_shared_ptr with pT passed in constructor \n" ;}
~my_shared_ptr()
{
std::cout <<"Inside Destructor of my_shared_ptr \n";
(*refcount)--;
std::cout <<"Inside Destructor refcount is : "<< (*refcount) <<"\n";
if ((*refcount) == 0)
{
std::cout <<"Deleting the one of the ptr whose reference count is now 0 \n";
delete ptr;
}
}
// Copy constructor
my_shared_ptr(const my_shared_ptr& obj)
{
std::cout <<"Inside copy constructor call \n";
this->ptr = obj.ptr;
this->refcount = obj.refcount;
if (obj.ptr != nullptr)
{
(*this->refcount)++;
}
}
// Copy assignment operator
my_shared_ptr& operator=(const my_shared_ptr& obj)
{
std::cout <<"Inside copy assignment call \n";
if (&obj == this )
return *this;
(*this->refcount)--;
if ((*this->refcount) == 0)
{
delete (this->ptr);
}
this->ptr = obj.ptr;
this->refcount = obj.refcount;
if ( obj.ptr!= nullptr)
{
(*this->refcount)++;
}
return *this;
}
// Move constructor
my_shared_ptr(my_shared_ptr&& dyingobj)
{
std::cout <<"Inside move constructor call \n";
this->ptr = dyingobj.ptr;
this->refcount = dyingobj.refcount;
dyingobj.ptr = nullptr; // cleaning up the dying object
dyingobj.refcount = nullptr;
}
// Move assignment
my_shared_ptr& operator=(my_shared_ptr&& dyingobj)
{
std::cout <<"Inside move assignment call \n";
if (&dyingobj == this )
{
std::cout <<"Two objects in move assignment are equal \n";
return *this;
}
if ( (this) != &dyingobj )
{
(this->refcount)--;
std::cout <<"Inside move assignment refcount is : "<< (*refcount) <<"\n";
if ((this->refcount) == 0)
{
std::cout <<"Deleting the previous shared object pointer whose reference count is now 0 \n";
delete this->ptr;
}
}
this->ptr = dyingobj.ptr;
this->refcount = dyingobj.refcount;
dyingobj.ptr = nullptr; // cleaning up the dying object
dyingobj.refcount = nullptr;
return *this;
}
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
};
class Resource
{
public:
Resource()
{
std::cout <<"Resource Acquired \n";
}
~Resource()
{
std::cout <<"Resource Destroyed \n";
}
};
my_shared_ptr<Resource> generateResource()
{
my_shared_ptr<Resource> res(new Resource());
return res;
}
int main()
{
std::cout <<"Before Creating mainres \n";
my_shared_ptr<Resource> mainres = generateResource();
std::cout <<"After Creating mainres \n";
mainres = generateResource();
std::cout <<"Before Creating mainres2 \n";
my_shared_ptr<Resource> mainres2 = mainres;
std::cout <<"After Creating mainres2 \n";
{
std::cout <<"Before Creating mainres3\n";
my_shared_ptr<Resource> mainres3;
mainres3 = mainres2;
std::cout <<"After mainres3 assignment\n";
}
std::cout <<"mainres3 destroyed\n";
//mainres = generateResource();
return 0;
}
Вывод вышеуказанного кода:
Before Creating mainres
Resource Acquired
Inside my_shared_ptr with pT passed in constructor
After Creating mainres
Resource Acquired
Inside my_shared_ptr with pT passed in constructor
Inside move assignment call
Inside move assignment refcount is : 0
Inside Destructor of my_shared_ptr
Глядя на O/P, кажется, что код выполнялся нормально до mainres = generateResource();, но затем после этого он молча завершил работу. И я не получил никакого дальнейшего o/p. Не уверен, почему? Как правильно это исправить и где я ошибаюсь. Внутри вызова назначения перемещения я пытаюсь удалить уменьшение счетчика ссылок объекта my_shared_ptr, которому назначается другой my_shared_ptr, и проверить, равен ли счетчик ссылок 0, а затем удалить ptr этого объекта my_shared_ptr. Я что-то пропустил здесь?
Заранее спасибо.
Напечатайте адреса в деструкторе, и вы можете заметить проблему.
Если вы собираетесь печатать сообщения в конструкторах и деструкторах класса, простой вывод простых сообщений мало поможет. Намного больше помогает распечатать значение this вместе с сообщением. Иначе вы не знаете, что на самом деле строится и разрушается.
Немного иронии... у тебя тоже утечка памяти
Помимо прочего, оператор присваивания уменьшает счетчик ссылок, даже если ptr имеет значение null. Это приведет к отрицательному значению счетчика ссылок, и ситуация выйдет из-под контроля.
Кстати, вы никогда не delete refcount; , что является утечкой памяти для каждого общего объекта. Более того, вы должны сначала начать с правила 3, а затем оптимизировать с помощью правила 5. То есть не спешите определять конструктор перемещения + назначение перемещения. В качестве начального шага гораздо проще и безопаснее реализовать правило 3 с идиомой копирования + подкачки; другие подходы включают в себя оптимизацию, которую сейчас делать слишком рано.
Спасибо всем, так что казалось, что я пытался переместить временный объект, который был удален, и, поскольку основные ресурсы получили от него ресурсы, это удалило основные ресурсы, что привело к проблеме.
@Red.Wave Да, идея состоит в том, чтобы использовать правило 5 везде, где мы можем, это было просто, чтобы попробовать кое-что самостоятельно. Да, я пропустил удаление счетчика ссылок, спасибо за это.
Я не уверен, какую проблему вы заметили, но мой комментарий был о том, что вы полагаетесь на тот факт, что refcount никогда не бывает nullptr, но затем вы обнуляете обе свои операции перемещения. И затем вы пытаетесь получить к нему доступ в деструкторе, чтобы уменьшить его, но он равен нулю.
my_shared_ptr::my_shared_ptr(T* pT) начинается со счетчика ссылок 0 вместо 1. Если объект, созданный с помощью этого конструктора, удаляется, вы уменьшаете счетчик ссылок до -1, т.е. объект не будет удален в этом случае или будет удален раньше, если вы правильно скопируете смарт указатель...
(this->refcount)--; Ваш оператор присваивания перемещения уменьшает указатель refcount вместо объекта, на который он указывает.
@Invictus, я имею в виду делать все, но шаг за шагом. Так вы получите четкое понимание. 1-й примените правило 3 с назначением копирования и конструктором (сделайте копирование/своп). Затем улучшите крайние случаи с тщательной реализацией назначения перемещения и конструктора. Делайте это, но терпеливо, стабильными шагами.
@JesperJuhl, как попасть в Карнеги-холл?
@Yksisarvinen да, спасибо за этот вклад, да, я изменил код, чтобы убедиться, что везде выполняются проверки на nullptr и т. д., и это было полезно.
@fabian да, это приводило к тому, что неправильный ссылочный номер печатался, когда мой последний объект выходил за рамки, устанавливая для него значение 1, поскольку это должно помочь избавиться от проблемы. Спасибо за это.
@Red.Wave Спасибо за ваш вклад и поддержку
Недавно я написал статью об умных указателях, я думаю, она может помочь вам понять, что происходит за кулисами: cppsenioreas.wordpress.com/2023/02/21/… способы.





Всегда очень полезно узнать о копировании и перемещении класса следующим образом:
#include <iostream>
struct Verbose {
Verbose() {
std::cout << " Verbose default construct\n";
}
Verbose(Verbose const&) {
std::cout << " Verbose copy construct\n";
}
Verbose(Verbose&&) {
std::cout << " Verbose move construct\n";
}
~Verbose() {
std::cout << " Verbose destruct\n";
}
Verbose& operator=(Verbose const&) {
std::cout << " Verbose copy assign\n";
return *this;
}
Verbose& operator=(Verbose&&) {
std::cout << " Verbose move assign\n";
return *this;
}
};
Это позволяет вам отслеживать, что именно вызывается в выводе. Между вызовами вы должны вставить дополнительный вывод, чтобы иметь возможность распознать из вывода, где вы находитесь внутри main().
int main() {
std::cout << "Default construct a\n";
auto a = Verbose();
std::cout << "address of a is: " << &a << '\n';
std::cout << "Copy construct b from a\n";
auto b = a;
std::cout << "address of a is: " << &a << '\n';
std::cout << "address of b is: " << &b << '\n';
std::cout << "Move construct c from a\n";
auto c = std::move(a);
std::cout << "address of a is: " << &a << '\n';
std::cout << "address of b is: " << &b << '\n';
std::cout << "address of c is: " << &c << '\n';
}
Default construct a
Verbose default construct
address of a is: 0x7ffd1a93cdf5
Copy construct b from a
Verbose copy construct
address of a is: 0x7ffd1a93cdf5
address of b is: 0x7ffd1a93cdf6
Move construct c from a
Verbose move construct
address of a is: 0x7ffd1a93cdf5
address of b is: 0x7ffd1a93cdf6
address of c is: 0x7ffd1a93cdf7
Verbose destruct
Verbose destruct
Verbose destruct
Если эта часть вам понятна, то далее вы можете использовать класс в SmartPointer, чтобы лучше понять его:
#include <memory>
int main() {
std::cout << "Default construct shared a\n";
auto a = std::make_shared<Verbose>();
std::cout << "shared a points to: " << a.get() << '\n';
std::cout << "Copy construct shared b from shared a\n";
auto b = a;
std::cout << "shared a points to: " << a.get() << '\n';
std::cout << "shared b points to: " << b.get() << '\n';
std::cout << "Move construct shared c from shared a\n";
auto c = std::move(a);
std::cout << "shared a points to: " << a.get() << '\n';
std::cout << "shared b points to: " << b.get() << '\n';
std::cout << "shared c points to: " << c.get() << '\n';
}
Default construct shared a
Verbose default construct
shared a points to: 0x559a146ae2d0
Copy construct shared b from shared a
shared a points to: 0x559a146ae2d0
shared b points to: 0x559a146ae2d0
Move construct shared c from shared a
shared a points to: 0
shared b points to: 0x559a146ae2d0
shared c points to: 0x559a146ae2d0
Verbose destruct
Вы также можете дать классу Verbose идентификатор для отслеживания отдельных объектов.
struct Verbose {
std::string id_ = "nameless";
void rename(std::string id) {
std::cout << " Verbose rename from " << id_ << " to " << id << "\n";
id_ = std::move(id);
}
Verbose() {
std::cout << " Verbose default construct " << id_ << "\n";
}
Verbose(Verbose const& o) {
std::cout << " Verbose copy construct " << id_ << "< = " << o.id_ << "\n";
}
Verbose(Verbose&& o) {
std::cout << " Verbose move construct " << id_ << "< = " << o.id_ << "\n";
}
~Verbose() {
std::cout << " Verbose destruct " << id_ << "\n";
}
Verbose& operator=(Verbose const& o) {
std::cout << " Verbose copy assign " << id_ << "< = " << o.id_ << "\n";
return *this;
}
Verbose& operator=(Verbose&& o) {
std::cout << " Verbose move assign " << id_ << "< = " << o.id_ << "\n";
return *this;
}
};
Обратите внимание, что идентификатор меняется только с помощью функции rename()!
int main() {
std::cout << "Default construct of a\n";
auto a = Verbose();
a.rename("a");
std::cout << "address of a is: " << &a << '\n';
std::cout << "Copy construct b from a\n";
auto b = a;
b.rename("b");
std::cout << "address of a is: " << &a << '\n';
std::cout << "address of b is: " << &b << '\n';
std::cout << "Move construct c from a\n";
auto c = std::move(a);
c.rename("c");
std::cout << "address of a is: " << &a << '\n';
std::cout << "address of b is: " << &b << '\n';
std::cout << "address of c is: " << &c << '\n';
}
Default construct of a
Verbose default construct nameless
Verbose rename from nameless to a
address of a is: 0x7ffee311aba0
Copy construct b from a
Verbose copy construct nameless<=a
Verbose rename from nameless to b
address of a is: 0x7ffee311aba0
address of b is: 0x7ffee311abc0
Move construct c from a
Verbose move construct nameless<=a
Verbose rename from nameless to c
address of a is: 0x7ffee311aba0
address of b is: 0x7ffee311abc0
address of c is: 0x7ffee311abe0
Verbose destruct c
Verbose destruct b
Verbose destruct a
Этот пример также хорошо показывает порядок вызовов деконструктора. Это прямо противоположно порядку построения.
Вот еще одна упрощенная реализация SharedPtr, которая также выполняет обширный вывод:
struct SharedState {
public:
SharedState(Verbose* ptr): ptr_(ptr), count_(1) {
std::cout << " SharedState construct " << id() << "\n";
}
~SharedState() {
std::cout << " SharedState destruct " << id() << "\n";
delete ptr_;
}
SharedState(SharedState const& o) = delete;
SharedState(SharedState&& o) = delete;
SharedState& operator=(SharedState const& o) = delete;
SharedState& operator=(SharedState&& o) = delete;
std::size_t increase() {
return ++count_;
}
std::size_t decrese() {
return --count_;
}
Verbose* get() const {
return ptr_;
}
std::string id() const {
return ptr_->id_ + "$" + std::to_string(count_);
}
private:
Verbose* ptr_;
std::size_t count_;
};
class SharedPtr {
public:
std::string id() const {
using namespace std::literals;
return id_ + "->" + (state_ ? state_->id() : "nullptr"s);
}
void rename(std::string id) {
std::cout << " SharedPtr rename from " << this->id_ << " to " << id << "\n";
id_ = std::move(id);
}
SharedPtr(): state_(nullptr) {
std::cout << " SharedPtr default construct " << id() << "\n";
}
SharedPtr(Verbose* ptr): state_(new SharedState(ptr)) {
std::cout << " SharedPtr direct construct " << id() << "\n";
}
SharedPtr(SharedPtr const& o) {
std::cout << " SharedPtr copy construct " << id() << "< = " << o.id() << "\n";
setSharedState(o.state_);
std::cout << " ======================== " << id() << "< = " << o.id() << "\n";
}
SharedPtr(SharedPtr&& o) {
std::cout << " SharedPtr move construct " << id() << "< = " << o.id() << "\n";
setSharedState(o.state_);
o.removeSharedState();
std::cout << " ======================== " << id() << "< = " << o.id() << "\n";
}
~SharedPtr() {
std::cout << " SharedPtr destruct " << id() << "\n";
removeSharedState();
}
SharedPtr& operator=(SharedPtr const& o) {
std::cout << " SharedPtr copy assign " << id() << "< = " << o.id() << "\n";
removeSharedState();
setSharedState(o.state_);
std::cout << " ===================== " << id() << "< = " << o.id() << "\n";
return *this;
}
SharedPtr& operator=(SharedPtr&& o) {
std::cout << " SharedPtr move assign " << id() << "< = " << o.id() << "\n";
removeSharedState();
setSharedState(o.state_);
o.removeSharedState();
std::cout << " ===================== " << id() << "< = " << o.id() << "\n";
return *this;
}
Verbose* get() const {
return state_ ? state_->get() : nullptr;
}
Verbose* operator->() const {
if (!state_) {
throw std::logic_error("dereference nullptr");
}
return state_->get();
}
private:
void removeSharedState() {
if (state_) {
if (state_->decrese() == 0) {
delete state_;
}
state_ = nullptr;
}
}
void setSharedState(SharedState* state) {
state_ = state;
if (state_) {
state_->increase();
}
}
std::string id_ = "Nameless";
SharedState* state_ = nullptr;
};
int main() {
std::cout << "Default construct A\n";
auto A = SharedPtr(new Verbose);
A->rename("a");
A.rename("A");
std::cout << A.id() << " points to: " << A.get() << '\n';
std::cout << "Copy construct B from A\n";
auto B = A;
B.rename("B");
std::cout << A.id() << " points to: " << A.get() << '\n';
std::cout << B.id() << " points to: " << B.get() << '\n';
std::cout << "Move construct C from A\n";
auto C = std::move(A);
C.rename("C");
std::cout << A.id() << " points to: " << A.get() << '\n';
std::cout << B.id() << " points to: " << B.get() << '\n';
std::cout << C.id() << " points to: " << C.get() << '\n';
}
Default construct A
Verbose default construct nameless
SharedState construct nameless$1
SharedPtr direct construct Nameless->nameless$1
Verbose rename from nameless to a
SharedPtr rename from Nameless to A
A->a$1 points to: 0x565113a0e2c0
Copy construct B from A
SharedPtr copy construct Nameless->nullptr<=A->a$1
======================== Nameless->a$2<=A->a$2
SharedPtr rename from Nameless to B
A->a$2 points to: 0x565113a0e2c0
B->a$2 points to: 0x565113a0e2c0
Move construct C from A
SharedPtr move construct Nameless->nullptr<=A->a$2
======================== Nameless->a$2<=A->nullptr
SharedPtr rename from Nameless to C
A->nullptr points to: 0
B->a$2 points to: 0x565113a0e2c0
C->a$2 points to: 0x565113a0e2c0
SharedPtr destruct C->a$2
SharedPtr destruct B->a$1
SharedState destruct a$0
Verbose destruct a
SharedPtr destruct A->nullptr
Если вы хотите использовать SharedPtr для произвольных типов, вам нужно создать шаблоны SharedPtr и SharedState. Обратите внимание, что это больше не предполагает, что тип T (ранее Verbose) имеет идентификатор. Поэтому он больше не может отображаться как часть идентификатора SharedPtr и SharedState. Это основная причина, по которой я сразу не показал универсальную версию.
Следующий diff показывает места, которые вам нужно изменить. Строки, которые начинаются с минуса. Строки, которые начинаются с плюса. (К сожалению, в настоящее время StackOverflow не поддерживает подсветку синтаксиса для этого).
diff --git a/main.cpp b/main.cpp
index 2a511ea..b02f931 100644
--- a/main.cpp
+++ b/main.cpp
@@ -36,9 +36,10 @@ struct Verbose {
}
};
+template <typename T>
struct SharedState {
public:
- SharedState(Verbose* ptr): ptr_(ptr), count_(1) {
+ SharedState(T* ptr): ptr_(ptr), count_(1) {
std::cout << " SharedState construct " << id() << "\n";
}
@@ -60,19 +61,20 @@ public:
return --count_;
}
- Verbose* get() const {
+ T* get() const {
return ptr_;
}
std::string id() const {
- return ptr_->id_ + "$" + std::to_string(count_);
+ return "ref_count$" + std::to_string(count_);
}
private:
- Verbose* ptr_;
+ T* ptr_;
std::size_t count_;
};
+template <typename T>
class SharedPtr {
public:
std::string id() const {
@@ -89,7 +91,7 @@ public:
std::cout << " SharedPtr default construct " << id() << "\n";
}
- SharedPtr(Verbose* ptr): state_(new SharedState(ptr)) {
+ SharedPtr(T* ptr): state_(new SharedState<T>(ptr)) {
std::cout << " SharedPtr direct construct " << id() << "\n";
}
@@ -128,11 +130,11 @@ public:
return *this;
}
- Verbose* get() const {
+ T* get() const {
return state_ ? state_->get() : nullptr;
}
- Verbose* operator->() const {
+ T* operator->() const {
if (!state_) {
throw std::logic_error("dereference nullptr");
}
@@ -149,7 +151,7 @@ private:
}
}
- void setSharedState(SharedState* state) {
+ void setSharedState(SharedState<T>* state) {
state_ = state;
if (state_) {
state_->increase();
@@ -157,12 +159,12 @@ private:
}
std::string id_ = "Nameless";
- SharedState* state_ = nullptr;
+ SharedState<T>* state_ = nullptr;
};
int main() {
std::cout << "Default construct A\n";
- auto A = SharedPtr(new Verbose);
+ auto A = SharedPtr<Verbose>(new Verbose);
A->rename("a");
A.rename("A");
std::cout << A.id() << " points to: " << A.get() << '\n';
Я не проверял операторы присваивания, пожалуйста, дайте мне знать, если что-то нужно изменить!
Я думаю, что все входные данные имели смысл, но усилия, которые вы предприняли, чтобы выразить это просто со всеми более мелкими деталями, заставили меня выбрать это в качестве ответа. Спасибо
Я думаю, что было бы лучше, если бы SharedState и SharedPtr использовали шаблон <typename T> вместо Verbose. Поскольку это сделало бы более общим для людей использование этого с любым классом, а не специфичным для Verbose.
@Invictus Это было частью упрощения. Вы можете просто заменить все Verbose в SharedState и SharedPtr на T и сделать их оба шаблоном. Но вывод ID Verbose как части ID SharedPtr и SharedState вылетает! Если есть какие-либо проблемы, просто дайте мне знать ;-)
Я попытался сделать это, объявив их как template<class T>, а затем заменив появление Verbose в обоих классах на T. Но я начал получать ошибку: main.cpp:158:25: error: тип заполнителя шаблона 'SharedState<. ..auto...>' должен сопровождаться простым идентификатором декларатора 158 | недействительным setSharedState (состояние SharedState *) { | ^~~~~~~~~~~ main.cpp:41:8: примечание: здесь объявлена «шаблонная структура SharedState» 41 | структура SharedState
@Invictus Я добавил Diff со строками, которые нужно изменить. Вот живой пример с обобщенным кодом: godbolt.org/z/bYahe3Wjq
Спасибо, Бенджамин, я понял, что мое знание синтаксиса шаблонов вызывает здесь проблему. Просто быстрый вопрос: похоже, что сохранение отдельного класса состояния более полезно, особенно при обработке случаев назначения перемещения, где вы можете удалить состояние отдельно или установить для него нули, что, казалось бы, было бы сложно, если бы этот счетчик ссылок был частью общего сам класс указателя. Верно ?
Наличие всех общих данных в классе SharedState имеет то преимущество, что копирование и перемещение проще реализовать, а SharedPtr использует меньше памяти (только один элемент данных). Большим недостатком является то, что для каждого доступа к данным необходимо два разыменования указателя. Реальные реализации, вероятно, разделят только количество, потому что небольшое преимущество в размере памяти менее важно, чем очень большой недостаток во времени доступа. sizeof(std::shared_ptr<int>) равно 16 для x64 libstdc++, что указывает на то, что он содержит один указатель на объект и один на другие данные (счетчик + объект удаления).
Поможет ли это: godbolt.org/z/v8oKoKxx9 ?