Deep understanding of C + + smart pointer -- an analysis of MSVC source code

Posted by Anxious on Mon, 17 Jan 2022 02:58:25 +0100

unique_ptr

unique_ptr is a move only type (move only type, std::mutex, etc.).

Combine the factory mode to see its basic usage, and give priority to std::make_unique :
(for the factory mode, see my notes: https://zhuanlan.zhihu.com/p/423725151)

#include <iostream>
#include <memory>

class Animal
{
public:
	virtual void Print() const = 0;
};

class Dog : public Animal
{
public:
	void Print() const override
	{
		std::cout << "Dog!" << std::endl;
	}
};	

class AnimalFactory
{
public:
	virtual std::unique_ptr<Animal> CreateAnimal() = 0;
};

class DogFactory : public AnimalFactory
{
public:
	std::unique_ptr<Animal> CreateAnimal() override
	{
		return std::make_unique<Dog>();
	}
};

class MyTest
{
public:
	MyTest(std::unique_ptr<AnimalFactory> animal_fac) : animal_fac(std::move(animal_fac)) {}
	void ButtonClick()
	{
		auto animal_new = animal_fac->CreateAnimal();
		animal_new->Print();
	}
private:
	std::unique_ptr<AnimalFactory> animal_fac;
};

int main()
{
	auto test = MyTest(std::make_unique<DogFactory>());
	test.ButtonClick();
}

Looking at the MSVC source code, we know that it has two template parameters:

The second is the default deletion device. We can:

auto my_del = [](Animal* animal)
{
	std::cout << "delete: ";
	animal->Print();
	delete animal;
};
std::unique_ptr<Animal, decltype(my_del)> t(new Dog, my_del);
t->Print();

We found that the type of the delegator is unique_ The type of PTR also has an impact, because it belongs to the second parameter of the template parameter (we will find that the delegator has no impact on the type when we talk about shared_ptr and weak_ptr later). Therefore, an error will be reported if it is generated using the previous factory function, because it is generated by STD:: make_ unique<Dog>(); Generated, type mismatch.

Similarly, we know that for the case of custom delegators, use std::make_unique won't work.

We know that for std::unique_ptr is provided in two forms: one is a single object: std::unique_ptr < T >, one is array: std::unique_ptr<T[]>. For a single object, there is no method of operator [], while for an array, there are no operator * and operator - >, which are rarely used.

And for shared_ptr and weak_ptr has no such distinction. We can see the following from the source code:

Through remove_extent_t eliminates the array. (_tis only available in C++14. Every transformation STD:: transformation < T >:: type in C++11 has a corresponding template of std::transformation_t in 14. The purpose is to replace typedef with using to avoid annoying typename, etc.), for example:

unique_ Another property of PTR is that it can be easily converted to shared_ptr, but remember that it is type only:

auto uptr = std::make_unique<Dog>();
// std::shared_ptr<Animal> sptr = uptr; // error! 
std::shared_ptr<Animal> sptr = std::move(uptr);

Or directly: STD:: shared_ ptr<Animal> sptr = std::move(std::make_unique<Dog>());

It seems that there is no problem, because shared_ptr has unique_ Constructor for PTR:
std::shared_ptr<Animal> sptr(std::make_unique<Dog>());

reference resources:
https://zh.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr

https://en.cppreference.com/w/cpp/memory/unique_ptr

https://zh.cppreference.com/w/cpp/memory/shared_ptr

shared_ptr and weak_ptr

In my notes, I mentioned: https://zhuanlan.zhihu.com/p/415508858

Reference link:
https://en.cppreference.com/w/cpp/memory
https://en.cppreference.com/w/cpp/memory/shared_ptr

For these two, in the implementation of MSVC, they will inherit from the same base class (but it seems that the standard specification is not written, and it is speculated that each compiler implements them). This is unique_ptr once said:

std::remove_extent_t is indeed introduced by C++14. As for the element in the previous figure_ Type seems to have made such an update in C++17.

The key to understand is that reference counting does not encapsulate a size in a class_ T is a number of general types. What is actually encapsulated in the class is a pointer to a control block (in the class _Ptr_base):

First element_ As mentioned in the type standard, the second is the pointer to the control block:

As you can see, it has some virtual methods and two counts: reference count and weak count.

Since shared_ptr and weak_ptr inherits the same base class, so the supporting naturally points to the same control block. For std::make_shared, its control block and managed element_ Resources of type will be allocated together, so that they will be destructed together as long as the reference count is set to 0 (but if the object is large, it is likely that it will not be destructed until the weak count is set to 0).

So when will a new control block be generated? Referring to effective modern C + +, a new control block will be created in the following three cases:

  1. std::make_shared
  2. Construct a shared from a pointer with exclusive ownership (i.e. std::unique_ptr or std::weak_ptr)_ ptr
  3. When STD:: shared_ When the PTR constructor is called with a bare pointer as an argument

The existence of reference count also brings some performance overhead. At the same time, for thread safety, the increment and decrement of reference count are atomic operations.

Put the unique_ The code of PTR is changed to shared_ptr :

class Animal
{
public:
	virtual void Print() const = 0;
};

class Dog : public Animal
{
public:
	void Print() const override
	{
		std::cout << "Dog!" << std::endl;
	}
};	

class AnimalFactory
{
public:
	virtual std::shared_ptr<Animal> CreateAnimal() = 0;
};

class DogFactory : public AnimalFactory
{
public:
	std::shared_ptr<Animal> CreateAnimal() override
	{
		return std::make_shared<Dog>();
	}
};

class MyTest
{
public:
	MyTest(std::shared_ptr<AnimalFactory> animal_fac) : animal_fac(std::move(animal_fac)) {}
	void ButtonClick()
	{
		auto animal_new = animal_fac->CreateAnimal();
		animal_new->Print();
	}
private:
	std::shared_ptr<AnimalFactory> animal_fac;
};

For weak_ptr, there is a lock method that can return a shared_ptr pointer can be applied as follows:

std::shared_ptr<Animal> CalAnimal(unsigned int animal_id)
{
	// A pile of calculations
	return DogFactory().CreateAnimal();
}

std::shared_ptr<Animal> fastCalAnimal(unsigned int animal_id)
{
	static std::unordered_map<unsigned int, std::weak_ptr<Animal>> cache;

	std::shared_ptr<Animal> objPtr = cache[animal_id].lock(); // Null pointer if not in cache

	if (!objPtr)
	{
		objPtr = CalAnimal(animal_id);
		cache[animal_id] = objPtr;
	}
	return objPtr;
}

For the example of Animal, suppose there is such a function CalAnimal, which passes in an id and determines what Animal to return through complex calculation. However, if the same id is passed in, it will undergo repeated calculation. In order to save time, we use a hash table to store the results to be returned.

This store cannot be shared_ptr, otherwise the reference count will always exist. Even if the object is not used outside, it is still stored inside the function; So we can choose weak_ptr, code above.

One problem that still exists is that it will lead to std::weak_ptr is accumulating.

std::bad_weak_ptr anomaly

weak_ The dangling pointer of PTR, also known as expired, can be tested with the expired method:

std::weak_ptr<Animal> wp;
if (wp.expired())
{
	std::cout << "dangling! " << std::endl;
}

If you use weak directly_ PTR is used as an argument to construct shared_ptr, when weak_ If PTR fails, an exception will be thrown:

try
{
	std::weak_ptr<Animal> wp;
	std::shared_ptr<Animal> sp(wp);
}
catch (std::bad_weak_ptr& e)
{
	std::cout << e.what() << std::endl;
}

std::enable_shared_from_this

Remember what we said before, when STD:: shared_ When the PTR constructor is called with a bare pointer as an argument, a new control block is generated. This leads to constructing shared with the this pointer of a class in the method inside the class_ PTR can cause problems - a new control block!

For example, the following code:

class Animal
{
public:
	virtual void Print() const = 0;
	void PrintAllName() const
	{
		for (auto& sptr : animal_container)
		{
			sptr->Print();
		}
	}
protected:
	std::vector<std::shared_ptr<Animal>> animal_container;
};

class Dog : public Animal
{
public:
	void Print() const override
	{
		std::cout << "Dog!" << std::endl;
	}
	void PushBack()
	{
		animal_container.emplace_back(this);
	}
};	

We are using:

auto d = std::make_shared<Dog>();
d->PushBack();

This is an undefined behavior. Because d is a shared_ptr, but in fact, when it calls the PushBack method, because it is constructed with this pointer, it will cause the shared pushed in_ The object pointed to by PTR (managed object resource) and d are the same object, but in fact, due to different control blocks, they are two different shared objects_ ptr ; When d destructs, the object is destructed, and the shared in the container_ When PTR destructs, the object is destructed again, and the second destruct will lead to undefined behavior.

So the solution can be std::enable_shared_from_this, we change the class just written as follows:

class Animal : public std::enable_shared_from_this<Animal>
{
public:
	virtual void Print() const = 0;
	void PrintAllName() const
	{
		for (auto& sptr : animal_container)
		{
			sptr->Print();
		}
	}
protected:
	std::vector<std::shared_ptr<Animal>> animal_container;
};

class Dog : public Animal
{
public:
	void Print() const override
	{
		std::cout << "Dog!" << std::endl;
	}
	void PushBack()
	{
		animal_container.emplace_back(shared_from_this());
	}
};	

Then write the code:

auto d = std::make_shared<Dog>();
d->PushBack();
d->Print();
d->PrintAllName();

There will be no problem.

Then STD:: Enable_ shared_ from_ How is this implemented?

reference resources: https://zh.cppreference.com/w/cpp/memory/enable_shared_from_this

enable_ shared_ from_ The common implementation of this is that it holds a weak reference to this (for example, std::weak_ptr). When shared is called_ from_ The this method will return a shared constructed by the weak pointer_ ptr:

The method to ensure that the weak pointer is a weak reference to this is CRTP. The template parameter of this class is_ Ty:

The weak pointer points to_ Ty:

So through the CRTP method, here_ Ty is actually an Animal class:

So as to achieve.

Topics: C++ Back-end