C++ smart pointers [Part 2] (shared_ptr/weak_ptr/circular reference/delete)

Article directory

  • 4. Smart pointer [shared_ptr]
    • 4.1 Design concept
      • member properties
    • 4.2 Main interfaces
      • copy construction
    • 4.3 Reference counting thread safety issues
      • Test thread safety
        • Making classes thread-safe through locking protection of counted references
        • Class instantiated objects require manual locking and protection when used.
      • The introduction of “lock”
      • Thread reference parameter passing problem
    • 4.4 Overall code
  • 5. Circular reference problem
    • 5.1 Introduction of problems
    • 5.2 Analyze the causes of this problem
    • 5.3The main code of weak_ptr
  • 6. Deletion problem of array objects
    • 6.1 Code issues
    • 6.2std::shared_ptr solution to this problem
      • 1. First look at std::shared_ptr::~shared_ptr
      • 2. Parameter passing and use of deleter
      • 3. Add package deleter
  • 7. Summary
    • 7.1 Complete code
    • 7.2The relationship between C++11 and Boost smart pointers

4.Smart pointer[shared_ptr]

4.1 Design Concept

Member attributes


In addition to a primary pointer, each object also has a secondary pointer for counting. Why not set it to int but to int*?

The same space is pointed to by two pointers. When the compiler gets the instruction that needs to destroy the pointer, it will first determine whether this is the last pointer, that is, count==1, and then release the space. In other cases, it is just the count–without releasing the space. This is Our original purpose, if it is set to int, it means that each object has its own count. When ptr1 is copied to ptr2, what we want is to have a separate space to count. If the copy is executed, the count + + means that we need a shared space to achieve this.

Isn’t it necessary to realize sharing? Why not just use static variables?

Static variables can be shared, yes, but they are shared by all objects of the current class. Example: The original intention of the code is that ptr1 ptr2 ptr3 points to the same space. At this time, count == 3. When ptr4 points to another space, the original intention of the code is count = 1, but Also changed ptr1/2/3

4.2 Main interface

Copy construction

 //Assignment overloading
//1."Self" assigns value to "Self"
//Not only p1 = p1, but also p2 = p1, but before p2 = p1
//2.Left = right after assignment
// The left pointer points to one less pointer to the space. The left pointer's count-- further consider the situation after count-- == 0.
// The right pointer points to one more pointer to the space. The count of the right pointer + +
shared_ptr<T> & operator=(const shared_ptr<T> & sp)
{<!-- -->
//"Self" assigns value to "Self": the essence of "Self"
if (_ptr != sp._ptr)
{<!-- -->
//count--
Release();

_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;

//count + +
AddCount();
}

return *this;
}

4.3 Reference counting thread safety issues

Testing thread safety

Make the class thread-safe through lock protection of counted references
 struct Date
{<!-- -->
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(ape::shared_ptr<Date> & sp, size_t n, mutex & mtx)
{<!-- -->
cout << " SharePtrFunc: sp.GetPtr() == " << sp.GetPtr() << endl;

for (size_t i = 0; i < n; + + i)
{<!-- -->
ape::shared_ptr<Date> copy(sp);
}
}

void test_shared_safe()
{<!-- -->
ape::shared_ptr<Date> p(new Date);
cout << "test_shared_safe: p.GetPtr() == " << p.GetPtr() << endl;

const size_t n = 10000;
mutex mtx;

//Thread reference parameter transfer Even if mtx is already a reference, the library function ref() still needs to be used here;
thread t1(SharePtrFunc, ref(p), n, ref(mtx));
thread t2(SharePtrFunc, ref(p), n, ref(mtx));

t1.join();
t2.join();

cout << "p.GetCount(): == " << p.GetCount() << endl;
}

Class instantiated objects require manual locking and protection


The introduction of “lock”

Thread reference parameter passing problem

A simple understanding is that p needs to call the constructor of the thread before passing it to sp. During this period, some action occurs, causing the reference attribute to be lost.

4.4 overall code

template<class T>
class shared_ptr
{<!-- -->
public:
\t//Constructor
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{<!-- -->

}

void Release()
{<!-- -->
//Lock
_pmtx->lock();
//Unable to release lock
bool deleteFlag = false;

if (--(*_pcount) == 0)
{<!-- -->
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
//Releasable lock
deleteFlag = true;
}
//Unlock
_pmtx->unlock();
//Judge and release the lock
if(deleteFlag)
{<!-- -->
delete _pmtx;
}
}
//void Release()
//{<!-- -->
// _pmtx->lock();
// bool deleteFlag = false;
// if (--(*_pcount) == 0)
// {<!-- -->
// if (_ptr)
// {<!-- -->
// //cout << "delete:" << _ptr << endl;
// //delete _ptr;
//
// //Deleter to delete
// _del(_ptr);
// }
//
// delete _pcount;
// deleteFlag = true;
// }
//
// _pmtx->unlock();
//
// if (deleteFlag)
// {<!-- -->
// delete _pmtx;
// }
//}
void AddCount()
{<!-- -->
_pmtx->lock();

+ + (*_pcount);

_pmtx->unlock();
}

//copy construction
shared_ptr(const shared_ptr<T> & amp; sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{<!-- -->
AddCount();
}

//Assignment overloading
//1."Self" assigns value to "Self"
//Not only p1 = p1, but also p2 = p1, but before p2 = p1
//2.Left = right after assignment
// The left pointer points to one less pointer to the space. The left pointer's count-- further consider the situation after count-- == 0.
// The right pointer points to one more pointer to space. The right pointer's count + +
shared_ptr<T> & operator=(const shared_ptr<T> & sp)
{<!-- -->
//"Self" assigns value to "Self": the essence of "Self"
if (_ptr != sp._ptr)
{<!-- -->
//count--
Release();

_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;

//count + +
AddCount();
}

return *this;
}

//destructor
~shared_ptr()
{<!-- -->
Release();
}

T & operator*()
{<!-- -->
return *_ptr;
}

T* operator->()
{<!-- -->
return _ptr;
}

T* GetPtr()
{<!-- -->
return _ptr;
}

int GetCount()
{<!-- -->
return *_pcount;
}

private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
};

void test_shared()
{<!-- -->
shared_ptr<int> sp1(new int(1));
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp2);

shared_ptr<int> sp4(new int(10));

sp1 = sp4;
sp4 = sp1;

sp1 = sp1;
sp1 = sp2;
}


// Thread safety issues ///
struct Date
{<!-- -->
int _year = 0;
int _month = 0;
int _day = 0;
};

void SharePtrFunc(ape::shared_ptr<Date> & sp, size_t n, mutex & mtx)
{<!-- -->
cout << " SharePtrFunc: sp.GetPtr() == " << sp.GetPtr() << endl;

cout << " SharePtrFunc: & amp;sp == " << & amp;sp << endl;

for (size_t i = 0; i < n; + + i)
{<!-- -->
ape::shared_ptr<Date> copy(sp);

mtx.lock();

sp->_year + + ;
sp->_day + + ;
sp->_month + + ;

mtx.unlock();
}
}

void test_shared_safe()
{<!-- -->
ape::shared_ptr<Date> p(new Date);
cout << "test_shared_safe: p.GetPtr() == " << p.GetPtr() << endl;

cout << "test_shared_safe: & amp;p == " << & amp;p << endl;

const size_t n = 100000;
mutex mtx;

//Thread reference parameter transfer Even if p and mtx are already references, the library function ref() still needs to be used here;
thread t1(SharePtrFunc, ref(p), n, ref(mtx));
thread t2(SharePtrFunc, ref(p), n, ref(mtx));

t1.join();
t2.join();

cout << "p.GetCount(): == " << p.GetCount() << endl;

cout << "p->_year == " << p->_year << endl;
cout << "p->_month == " << p->_month << endl;
cout << "p->_month == " << p->_month << endl;

}

5. Circular reference problem

Introduction of 5.1 issues






5.2 Analysis of the causes of this problem

In order to solve this problem, we need to introduce weak_ptr. What we need to know is
The smart pointer shared_ptr satisfies

  1. In line with RAII thinking
  2. Can be used like a pointer
  3. Support copy

The smart pointer weaked_ptr satisfies

  1. Not in line with RAII thinking
  2. Can be used like a pointer
  3. Help solve the circular reference problem of shared_ptr
  4. weaked_ptr can point to resources, but does not participate in management and does not increase the reference count.

In fact, the smart pointers in the library are much more complicated than what we have mentioned above. In order to facilitate learning and understanding, we only need to learn the core framework and need to know the two functions supported by the library
Weaked_ptr has its own count and cooperates with expired to determine whether the pointed resource still needs to be pointed to. That is, weaked_ptr can point to the resource, but does not participate in management and does not increase the shared_ptr reference count, so the count existing in a pointed resource has reached 0 at this time. expired determines whether it is invalid. If the resource pointed to is invalid, weaked_ptr will no longer point to it.

Main code of 5.3weak_ptr

template<class T>
class weak_ptr
{<!-- -->
public:
weak_ptr()
:_ptr(nullptr)
{<!-- -->
\t
}

weak_ptr(const shared_ptr<T> & amp; sp)
:_ptr(sp.GetPtr())
{<!-- -->
\t
}

T & operator*()
{<!-- -->
return *_ptr;
}

T* operator->()
{<!-- -->
return _ptr;
}

T* GetPtr()
{<!-- -->
return _ptr;
}

private:
T* _ptr;
};

6. Deletion of array objects

6.1 code problem

6.2std::shared_ptr’s solution to this problem

1. First look at std::shared_ptr::~shared_ptr

That is, the difference between the shared_ptr::~shared_ptr in the library and the destructor we wrote is that when an object is instantiated, it will determine whether it has accepted the parameter deleter. If it receives it, it will call deleter when it is destructed. If it does not receive deleter, it will be normal. destruct

2. Parameter passing and use of deleter

Destroy the object. However, previously, depending on the value of the member use_count, it could have the following side effects: If use_count is greater than 1 (i.e. the object shares ownership of its managed object with other shared_ptr objects): the use count of other objects with which it shares ownership will be reduced by 1. If use_count is 1 (i.e., the object is the sole owner of a managed pointer): the pointer it owns is deleted (if the shared_ptr object was constructed with the special deleter, then it is called; otherwise, the function uses operator delete). If use_count is zero (i.e. the object is empty), this destructor has no side effects.

template<class T>
struct DeleteArray
{<!-- -->
void operator()(T* ptr)
{<!-- -->
cout << "Anonymous object DeleteArray<Date>(): " << ptr << endl;
delete[] ptr;
}
};
void test_std_shared_deletor()
{<!-- -->
//template <class U, class D>
//shared_ptr (U* p, D del); Constructor with deleter

std::shared_ptr<Date> sparr1(new Date[10], DeleteArray<Date>());

std::shared_ptr<Date> sparr2(new Date[10],
[](Date* ptr)
{<!-- -->
cout << "lambda expression delete[]: " << ptr << endl;
delete[] ptr;
}
);

auto deleter = [](Date* ptr)
{<!-- -->
cout << "lambda expression delete[]: " << ptr << endl;
delete[] ptr;
};
std::shared_ptr<Date> sparr3(new Date[10], deleter);

std::shared_ptr<FILE> spFile(fopen("Test.cpp", "r"),
[](FILE* ptr)
{<!-- -->
cout << "lambda expression delete[]: " << ptr << endl;
fclose(ptr);
}
);
}

3. Add package deleter

7. Summary

7.1 complete code

#pragma once

#include <mutex>
#include <thread>
#include <memory>

namespace ape
{<!-- -->
template<class T>
class shared_ptr
{<!-- -->
public:
\t\t//Constructor
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{<!-- -->

}

//Deletor constructor
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
, _del(del)
{<!-- -->
\t\t
}

//Non-deleteerRelease()
/*
void Release()
{
//Lock
_pmtx->lock();
//Unable to release lock
bool deleteFlag = false;

if (--(*_pcount) == 0)
{
if (_ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
\t\t\t\t
delete _pcount;
//Releasable lock
deleteFlag = true;
}
//Unlock
_pmtx->unlock();
//Judge and release the lock
if(deleteFlag)
{
delete _pmtx;
}
}
*/
\t\t
//DeleterRelease()
void Release()
{<!-- -->
_pmtx->lock();
bool deleteFlag = false;

if (--(*_pcount) == 0)
{<!-- -->
if (_ptr != nullptr)
{<!-- -->
_del(_ptr);
}
\t\t
delete _pcount;
deleteFlag = true;
}
\t\t
_pmtx->unlock();
\t\t
if(deleteFlag)
{<!-- -->
delete _pmtx;
}
}
void AddCount()
{<!-- -->
_pmtx->lock();

+ + (*_pcount);

_pmtx->unlock();
}

//copy construction
shared_ptr(const shared_ptr<T> & amp; sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{<!-- -->
AddCount();
}

//Assignment overloading
//1."Self" assigns value to "Self"
//Not only p1 = p1, but also p2 = p1, but before p2 = p1
//2.Left = right after assignment
// The left pointer points to one less pointer to the space. The left pointer's count-- further consider the situation after count-- == 0.
// The right pointer points to one more pointer to the space. The count of the right pointer + +
shared_ptr<T> & operator=(const shared_ptr<T> & sp)
{<!-- -->
//"Self" assigns value to "Self": the essence of "Self"
if (_ptr != sp._ptr)
{<!-- -->
//count--
Release();

_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;

//count + +
AddCount();
}

return *this;
}

//destructor
~shared_ptr()
{<!-- -->
Release();
}

T & operator*()
{<!-- -->
return *_ptr;
}

T* operator->()
{<!-- -->
return _ptr;
}

T* GetPtr() const
{<!-- -->
return _ptr;
}

int GetCount() const
{<!-- -->
return *_pcount;
}

private:
T* _ptr;
int* _pcount;
mutex* _pmtx;

//wrapper
//Default value processing: ape::shared_ptr<Date> sp(new Date);
//Because you use the deleter by default when destructing, use the default value when there is no display.

//When the constructor passes deleter, it can be a functor, lambda expression, function pointer, and a wrapper is used to receive it here.
function<void(T*)> _del = [](T* ptr)
{<!-- -->
cout << "lambda expression delete: " << ptr << endl;
delete ptr;
};
};

void test_shared()
{<!-- -->
shared_ptr<int> sp1(new int(1));
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp2);

shared_ptr<int> sp4(new int(10));

sp1 = sp4;
sp4 = sp1;

sp1 = sp1;
sp1 = sp2;
}


// Thread safety issues ///
struct Date
{<!-- -->
int _year = 0;
int _month = 0;
int _day = 0;

~Date()
{<!-- -->

}
};

void SharePtrFunc(ape::shared_ptr<Date> & sp, size_t n, mutex & mtx)
{<!-- -->
cout << " SharePtrFunc: sp.GetPtr() == " << sp.GetPtr() << endl;

cout << " SharePtrFunc: & amp;sp == " << & amp;sp << endl;

for (size_t i = 0; i < n; + + i)
{<!-- -->
ape::shared_ptr<Date> copy(sp);

mtx.lock();

sp->_year + + ;
sp->_day + + ;
sp->_month + + ;

mtx.unlock();
}
}

void test_shared_safe()
{<!-- -->
ape::shared_ptr<Date> p(new Date);
cout << "test_shared_safe: p.GetPtr() == " << p.GetPtr() << endl;

cout << "test_shared_safe: & amp;p == " << & amp;p << endl;

const size_t n = 100000;
mutex mtx;

//Thread reference parameter transfer Even if p and mtx are already references, the library function ref() still needs to be used here;
thread t1(SharePtrFunc, ref(p), n, ref(mtx));
thread t2(SharePtrFunc, ref(p), n, ref(mtx));

t1.join();
t2.join();

cout << "p.GetCount(): == " << p.GetCount() << endl;

cout << "p->_year == " << p->_year << endl;
cout << "p->_month == " << p->_month << endl;
cout << "p->_month == " << p->_month << endl;

}

template<class T>
class weak_ptr
{<!-- -->
public:
weak_ptr()
:_ptr(nullptr)
{<!-- -->
\t\t
}

weak_ptr(const shared_ptr<T> & amp; sp)
:_ptr(sp.GetPtr())
{<!-- -->
\t\t
}

T & operator*()
{<!-- -->
return *_ptr;
}

T* operator->()
{<!-- -->
return _ptr;
}

T* GetPtr()
{<!-- -->
return _ptr;
}

private:
T* _ptr;
};

// circular reference
struct ListNode
{<!-- -->

/*Normal writing
ListNode* _next;
ListNode* _prev;
int _val;
*/

\t\t
/*shared_ptr
ape::shared_ptr<ListNode> _next;
ape::shared_ptr<ListNode> _prev;
int _val;
*/

//weaked_ptr
ape::weak_ptr<ListNode> _next;
ape::weak_ptr<ListNode> _prev;
int _val;

~ListNode()
{<!-- -->
cout << "~ListNode()" << endl;
}
};

// circular reference
void test_shared_cycle()
{<!-- -->
/*
Conventional writing method - not suitable for exception throwing scenarios
ListNode* n1 = new ListNode;
ListNode* n2 = new ListNode;

n1->_next = n2;
n2->_prev = n1;

delete n1;
delete n2;
*/

//Smart pointer writing method
ape::shared_ptr<ListNode> n1(new ListNode);
ape::shared_ptr<ListNode> n2(new ListNode);

//Built-in type = custom type -- error
/*
ListNode* _next;
ListNode* _prev;
int _val;

ape::shared_ptr<ListNode> n1(new ListNode);
ape::shared_ptr<ListNode> n2(new ListNode);

n1->_next = n2;
n2->_prev = n1;
*/

/*shared_ptr: Causes circular reference problem
ape::shared_ptr<ListNode> _next;
ape::shared_ptr<ListNode> _prev;
int _val;

ape::shared_ptr<ListNode> n1(new ListNode);
ape::shared_ptr<ListNode> n2(new ListNode);

n1->_next = n2;
n2->_prev = n1;
*/
\t\t
/*weak_ptr: resolve circular references
ape::weak_ptr<ListNode> _next;
ape::weak_ptr<ListNode> _prev;
int _val;

ape::shared_ptr<ListNode> n1(new ListNode);
ape::shared_ptr<ListNode> n2(new ListNode);
        */
cout << "n1.GetCount() == " << n1.GetCount() << endl;
cout << "n2.GetCount() == " << n2.GetCount() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << "n1.GetCount() == " << n1.GetCount() << endl;
cout << "n2.GetCount() == " << n2.GetCount() << endl;

}

/
//Custom deleter -- callable object
\t
template<class T>
struct DeleteArray
{<!-- -->
void operator()(T* ptr)
{<!-- -->
cout << "Anonymous object DeleteArray<Date>(): " << ptr << endl;
delete[] ptr;
}
};

//Study of std library deleter
/*
void test_std_shared_deletor()
{
//template <class U, class D>
//shared_ptr (U* p, D del); Constructor with deleter

std::shared_ptr<Date> sparr1(new Date[10], DeleteArray<Date>());

std::shared_ptr<Date> sparr2(new Date[10],
[](Date* ptr)
{
cout << "lambda expression delete[]: " << ptr << endl;
delete[] ptr;
}
);

auto deleter = [](Date* ptr)
{
cout << "lambda expression delete[]: " << ptr << endl;
delete[] ptr;
};
std::shared_ptr<Date> sparr3(new Date[10], deleter);

std::shared_ptr<FILE> spFile(fopen("Test.cpp", "r"),
[](FILE* ptr)
{
cout << "lambda expression delete[]: " << ptr << endl;
fclose(ptr);
}
);
}
*/

//Deletor constructor
/*
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
, _del(del)
{

}
*/
void test_ape_shared_deleter()
{<!-- -->
ape::shared_ptr<Date> sp(new Date);

ape::shared_ptr<Date> sparr1(new Date[10], DeleteArray<Date>());
ape::shared_ptr<Date> sparr2(new Date[10],
[](Date* ptr)
{<!-- -->
cout << "lambda expression delete[]: " << ptr << endl;
delete[] ptr;
}
);

auto deleter = [](Date* ptr)
{<!-- -->
cout << "lambda expression delete[]: " << ptr << endl;
delete[] ptr;
};
ape::shared_ptr<Date> sparr3(new Date[10], deleter);

ape::shared_ptr<FILE> spFile(fopen("Test.cpp", "r"),
[](FILE* ptr)
{<!-- -->
cout << "lambda expression delete[]: " << ptr << endl;
fclose(ptr);
}
);
}
}


The relationship between 7.2C++11 and Boost smart pointers

  1. The first smart pointer, auto_ptr, was introduced in C++98.
  2. C++ boost gives more practical scoped_ptr/shared_ptr/weak_ptr.
  3. C++TR1, introduced shared_ptr. [TR1 is not the standard version]
  4. C++11 introduced unique_ptr/shared_ptr/weak_ptr. unique_ptr corresponds to boost’s scoped_ptr. [Refer to boost implementation for implementation principle]