Translated by Babu_Abdulsalam from CodeProject

The introduction of

Although there is another article about smart Pointers in C++11. Lately, I’ve been hearing a lot of talk about a new C++ standard called C++0x/C++11. I took a look at some of the language features of C++11 and found that it does have some big changes. I’ll focus on the smart pointer part of C++11.

background

Normal /raw/ Naked Pointers?

Let’s discuss it one by one.

Pointers can cause a lot of problems if not handled properly, so people avoid using them. This is why many novice programmers don’t like Pointers. There are many issues associated with Pointers, such as the lifecycle of the object to which the pointer points, dangling references, and memory leaks.

A pending reference occurs when a block of memory is referenced by multiple Pointers, but one of the Pointers is freed and the rest are unaware of it. And memory leaks, as you know, happen when memory is taken from the heap and not released back. Some people say, why should I use smart Pointers when I’ve written code that’s clear and has error validation? A programmer also asked me, “Hey, here’s my code. I took a chunk of memory from the heap, and when I used it, I correctly returned it to the heap, so what’s the point of using smart Pointers?”

void Foo( )
{ 
    int* iPtr = new int[5];  
    //manipulate the memory block . . .  
    delete[ ] iPtr;
 }
Copy the code

Ideally, the above code should work just fine, and the memory should be freed up properly. But think about the actual working environment and the code execution conditions. In the gap between memory allocation and release, program instructions can do a lot of really bad things, such as accessing invalid memory addresses, dividing by zero, or having another programmer fix a bug in your program by adding a premature return statement based on a condition.

In all of these cases, your program doesn’t get to the free part of memory. In the first two cases, the program throws an exception, and in the third case, the program returns prematurely before memory has been freed. So while the program is running, memory is already leaking.

The solution to all of these problems is to use smart Pointers (if they are smart enough).

What is a smart pointer?

The smart pointer is a RAII (Resource Acquisition is Initialization) class model used to dynamically allocate memory. It provides all the interfaces that normal Pointers provide, with very few exceptions. In construction, it allocates memory, and when it leaves scope, it automatically frees the allocated memory. This frees the programmer from the tedious task of manually managing dynamic memory.

C++98 provides the first kind of smart pointer: auto_ptr

auto_ptr

Let’s see how auto_PTR solves this problem.

class Test
{
    public: 
    Test(int a = 0 ) : m_a(a) { }
    ~Test( )
    { 
       cout << "Calling destructor" << endl; 
    }
    public: int m_a;
};
void main( )
{ 
    std::auto_ptr<Test> p( new Test(5) ); 
    cout << p->m_a << endl;
}  
Copy the code

The code above intelligently frees the memory bound to the pointer. Here’s how it works: We allocate a block of memory to hold the Test object and bind it to auto_ptr p. So when p leaves the scope, the memory block it points to is automatically freed.

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
//***************************************************************
void Fun( )
{
 int a = 0, b= 5, c;
 if( a ==0 )
 {
  throw "Invalid divisor";
 }
 c = b/a;
 return;
}
//***************************************************************
void main( )
{
 try
 {
  std::auto_ptr<Test> p( new Test(5) ); 
  Fun( );
  cout<<p->m_a<<endl;
 }
 catch(...)
 {
  cout<<"Something has gone wrong"<<endl; }}Copy the code

In the above example, although the exception is thrown, the pointer is still released correctly. This is because when the exception is thrown, the stack unwinding, and when all the objects in the try block are destroyed, P leaves the scope, so its bound memory is freed.

Issue1:

So far, auto_PTR is smart enough, but it has some fundamental flaws. When an auto_PTR is assigned to another auto_PTR, its ownship is also transferred. This is a problem when I pass auto_ptr between functions. So, I have an auto_ptr in Foo(), and then IN Foo() I pass the pointer to Fun(), and when Fun() is done, ownership of the pointer doesn’t return to Foo.

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
 
 
//***************************************************************
void Fun(auto_ptr<Test> p1 )
{
 cout<<p1->m_a<<endl;
}
//***************************************************************
void main( )
{
 std::auto_ptr<Test> p( new Test(5) ); 
 Fun(p);
 cout<<p->m_a<<endl;
} 
Copy the code

The above code causes the program to crash due to the wild pointer behavior of auto_ptr. During this time these details occur, P owns a block of memory, and when Fun calls, P passes ownership of the associated block of memory to auto_ptr P1, which is a copy of P. P1 has the same block of memory that P had before. So far, so good. Now that Fun is done, P1 is out of scope, so the memory block associated with P1 is freed. What about p? P is gone, that’s why crash happened, and the next line of code tries to access P as if P had any resources.

Issue2:

There is another drawback. Auto_ptr cannot point to a set of objects, which means it cannot be used with the operator new[].

/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
void main( )
{
 std: :auto_ptr<Test> p(new Test[5]);
}
Copy the code

The above code will generate a runtime error. Because delete is used by default to free the associated memory when auto_ptr leaves scope. This is of course fine when auto_ptr refers to only one object, but in the code above, we created a group of objects in the heap and should use delete[] instead of delete to free them.

Issue3:

Auto_ptr cannot match standard containers (vector,list,map….) Use together.

Since auto_ptr is error-prone, it will also be deprecated. C++11 provides a new set of smart Pointers, each of which has its own uses.

  • shared_ptr
  • unique_ptr
  • weak_ptr

shared_ptr

Well, get ready for real intelligence. The first smart pointer is shared_ptr, which has a concept called sharedownership. The goal of shared_ptr is very simple: multiple Pointers can point to an object at the same time, and memory is automatically freed when the last shared_ptr leaves scope.

Create:

void main( )
{
 shared_ptr<int> sptr1( new int );
}
Copy the code

Use the make_shared macro to speed up creation. Because shared_Ptr actively allocates memory and holds reference counts,make_shared achieves creation in a more efficient way.

void main( )
{
 shared_ptr<int> sptr1 = make_shared<int>(100);
}
Copy the code

The above code creates a shared_ptr that points to a block of memory containing an integer 100 and a reference count of 1. If you create a shared_ptr with sptr1, the reference count becomes 2. This count is called a strong reference. In addition, Shared_Ptr has another reference count called weak reference, which will be described later.

You can find the number of shared_ptr by calling use_count() to get the reference count. When debugging, you can check the reference count by observing the value of strong_ref in shareD_ptr.

destructor

Shared_ptr calls delete by default to release associated resources. If the user adopts a different destructor policy, he is free to specify the policy to construct the shared_ptr. The following example is a problem caused by using the default destructor strategy:

class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
         int m_a;
};
void main( )
{
 shared_ptr<Test> sptr1( new Test[5] );
}
Copy the code

In this scenario, shared_ptr points to a set of objects, but when out of scope, the default destructor calls DELETE to free the resource. In fact, we should call delete[] to destroy the array. A user can specify a generic release step by calling a function, such as a LAMda expression.

void main( ) { shared_ptr<Test> sptr1( new Test[5], [ ](Test* p) { delete[ ] p; }); }Copy the code

The above code works perfectly by specifying delete[] to destruct.

Just like a normal pointer, shared_ptr also provides the dereference operator *,->. In addition, it provides some more important interfaces:

  • get(): getshared_ptrBound resources.
  • reset(): Releases ownership of the associated block of memory if it was the last one to point to the resourceshared_ptr, the memory is freed.
  • unique: Checks whether the current memory is uniqueshared_ptr.
  • operator bool: Judge the currentshared_ptrAn if expression can be used to determine whether a memory block is referred to.

OK, this is all about shared_ptr, but shared_ptr also has some Issues:

void main( )
{
 shared_ptr<int> sptr1( new int );
 shared_ptr<int> sptr2 = sptr1;
 shared_ptr<int> sptr3;
 sptr3 =sptr1
Copy the code

The following table shows the changes in reference counts in the code above:

All shared_ptrs have the same reference count and belong to the same group. The code above works fine, so let’s look at another set of examples.

void main( )
{
 int* p = new int;
 shared_ptr<int> sptr1( p);
 shared_ptr<int> sptr2( p );
}
Copy the code

The above code produces an error because two shared_ptr from different groups point to the same resource. The chart below gives you a picture of what went wrong:

To avoid this problem, try not to create shared_ptr from a naked pointer.

class B;
class A
{
public:
 A(  ) : m_sptrB(nullptr) { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 shared_ptr<B> m_sptrB;
};
class B
{
public:
 B(  ) : m_sptrA(nullptr) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 shared_ptr<A> m_sptrA;
};
//***********************************************************
void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
}
Copy the code

The above code produces a circular reference. A has A shared_ptr for B, and B has A shared_ptr for A. The resources associated with sptrA and sptrB are not released.

sptrA
sptrB

  1. If a fewshared_ptrsThe memory block pointed to belongs to a different group and an error will be generated.
  2. If you create one from an ordinary pointershared_ptrAnd that raises another question. In the code above, consider only oneshared_ptrIs made up ofpCreated, the code works fine. In case the programmer deletes the normal pointer before the smart pointer scope endsp. Oh my God!! Another crash.
  3. Circular reference: If the shared smart pointer is involved in a circular reference, the resource will not be freed properly.

To address circular references, C++ provides another kind of smart pointer: weak_ptr

Weak_Ptr

Weak_ptr has sharing semantics and not owning semantics. This means that Weak_Ptr can share resources held by shared_PTR. So you can create weak_ptr from a shared_ptr that contains the resource.

Weak_ptr does not support *, -> operations contained in ordinary Pointers. It does not contain resources and does not allow programmers to manipulate resources. So how do we use weak_ptr?

The answer is to create shareD_Ptr from Weak_Ptr and then use it. By adding a strong reference count, you can ensure that resources are not destroyed when used. When the reference count increases, you can be sure that the shared_ptr reference count created from Weak_Ptr is at least 1. Otherwise, when you use Weak_ptr, the following problems can occur: when shared_ptr goes out of scope, its resources are released, causing confusion.

create

Weak_ptr increases the weak reference count of the shared pointer, which means that shared_Ptr shares its resources with other Pointers. However, when shared_ptr goes out of scope, this count is not used as a basis for releasing resources. In other words, the resource to which the pointer points will not be freed until the strong reference count is zero, and weak reference counts do not work here.

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 weak_ptr<Test> wptr1 = wptr;
}
Copy the code

The reference count of SHARED_ptr and Weak_ptr can be observed from the following figure:

Giving one Weak_Ptr to another Weak_PTR increases the weak reference count.

So, when shared_ptr goes out of scope, its resources are released. What happens to weak_ptr pointing to shared_ptr? Weak_ptr is expired.

There are two ways to determine whether weak_ptr points to effective resources:

  1. calluse-count()To get a reference count, this method returns only a strong reference count, not a weak reference count.
  2. callexpired()Methods. Than the calluse_count()The method is faster.

Call Lock () from weak_ptr to get shareD_ptr or directly transform weak_ptr to shared_ptr

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 shared_ptr<Test> sptr2 = wptr.lock( );
}
Copy the code

As mentioned earlier, taking shared_ptr from Weak_ptr increases the strong reference count.

Now let’s see how weak_ptr solves the circular reference problem:

class B;
class A
{
public:
 A(  ) : m_a(5)  { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 void PrintSpB( );
 weak_ptr<B> m_sptrB;
 int m_a;
};
class B
{
public:
 B(  ) : m_b(10) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 weak_ptr<A> m_sptrA;
 int m_b;
};

void A::PrintSpB() {if( !m_sptrB.expired() )
 {  
  cout<< m_sptrB.lock( )->m_b<<endl;
 }
}

void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
 sptrA->PrintSpB( ); 
}

Copy the code

unique_ptr

Unique_ptr is also a replacement for auto_ptr. Unique_ptr follows exclusive semantics. At any point in time, a resource can only be held by one unique_Ptr. When unique_ptr goes out of scope, the contained resources are released. If a resource is overwritten by another resource, the previously owned resource is released. So it ensures that its associated resources are always released.

Unique_ptr is created in the same way as shared_ptr, unless unique_ptr is created pointing to the array type.

unique_ptr<int> uptr( new int );
Copy the code

Unique_ptr provides a special method for creating array objects by calling delete[] instead of delete when the pointer is out of scope. When unique_ptr is created, this set of objects is treated as part of the template parameters. In this way, the programmer does not need to provide a specified destructor as follows:

unique_ptr<int[ ]> uptr( new int[5] );
Copy the code

When unique_ptr is assigned to another object, ownership of the resource is transferred.

Remember unique_ptr does not provide copy semantics (copy assignments and copy constructs do not work), only move semantics is supported.

In the example above, if UPT3 and UPT5 already have resources, the previous resources will be released only when new resources are available.

interface

Unique_ptr provides the same interface as traditional Pointers, but does not support pointer arithmetic.

Unique_ptr provides a release() method to release ownership. The difference between release and reset is that release only releases ownership but not resources, and reset also releases resources.

Which one to use?

It all depends on how you want to own a resource, use shared_ptr if you want to share the resource, unique_ptr if you want to use the resource exclusively.

In addition, shared_ptr is heavier than unique_ptr because it also needs to allocate space for other things, such as storing strong reference counts and weak reference counts. Unique_ptr doesn’t need this, it just needs to hold the resource object exclusively.