深入理解C++智能指针——浅析MSVC源码

文章目录

  • unique_ptr
  • shared_ptr 与 weak_ptr
    • std::bad_weak_ptr 异常
    • std::enable_shared_from_this
  • 补充

unique_ptr

unique_ptr 是一个只移型别(move-only type,只移型别还有std::mutex等)。

结合一下工厂模式,看看其基本用法,优先使用 std::make_unique
(关于工厂模式,可见我曾经的笔记:https://zhuanlan.zhihu.com/p/423725151)

#include 
#include 

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();
}

查看MSVC源码,我们知道其有两个模板参数:
在这里插入图片描述
第二个是默认删除器,我们可以这样:

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();

我们发现删除器的型别对 unique_ptr 的型别也有影响,因为它属于模板参数的第二个参数(在之后讲shared_ptr 和 weak_ptr 的时候我们会发现删除器对型别没有影响),因此这里若是使用前面的工厂函数生成就会报错,因为它是由std::make_unique();生成的,型别对不上。

同样的我们知道,对于定制删除器的情况,用std::make_unique就无法生效。

我们知道,对于 std::unique_ptr 有两种形式提供:一种是单个对象:std::unique_ptr,一种是数组:std::unique_ptr。对单个对象就没有 operator [] 的方法,而数组的情况则没有 operator*operator ->,后者用的极少。

而对于shared_ptr和weak_ptr则没有这样区分,究其源码我们可以看到下面这样:
在这里插入图片描述
通过remove_extent_t消除了数组的情况。( _t 是C++14才有的,每个C++11中的变换std::transformation::type在14都有对应的std::transformation_t的模板,目的是用using取代typedef,避免烦人的typename等等),比如这里的情形:
在这里插入图片描述
unique_ptr 还有一个性质就是能很方便地转为 shared_ptr,但是记住他是只移型别:

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

或者直接:std::shared_ptr sptr = std::move(std::make_unique());

这样好像也没问题,因为shared_ptr有unique_ptr的构造函数:
std::shared_ptr sptr(std::make_unique());
在这里插入图片描述
参考:
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 与 weak_ptr

在我曾经的笔记曾提到过:https://zhuanlan.zhihu.com/p/415508858

参考链接:
https://en.cppreference.com/w/cpp/memory
https://en.cppreference.com/w/cpp/memory/shared_ptr
深入理解C++智能指针——浅析MSVC源码_第1张图片
深入理解C++智能指针——浅析MSVC源码_第2张图片

关于这两个,在MSVC的实现中会把他们继承于同一个基类(但是似乎标准规格书没有写,猜测是各家编译器自己实现的),这在前面讲unique_ptr的时候曾讲过:
在这里插入图片描述
std::remove_extent_t 确实是 C++14 引入的,至于前面那张图的 element_type 看来是在 C++17 才做出了这样的更新。

关键要理解的是,引用计数并不是类里面封装一个size_t一般类型的数,类里面实际封装的是一个指向一个控制块的指针(类 _Ptr_base 中):
在这里插入图片描述
第一个 element_type 标准已经讲了,第二个就是这个指向控制块的指针:
深入理解C++智能指针——浅析MSVC源码_第3张图片
可以看到,其有一些虚的方法,还有两个计数:引用计数和弱计数。

既然 shared_ptr 与 weak_ptr 是继承同一个基类,那么配套的自然也就指向同一个控制块了。对于 std::make_shared,其控制块和托管 element_type 类型的资源会一同分配,从而只要引用计数归0,就会一同析构(但若是对象很大,则很可能要到弱计数归0的时候才会被析构)。

那么什么时候会产生新的控制块呢?参考effective modern C++,在以下三种情况将会创建新的控制块:

  1. std::make_shared
  2. 从具备专属所有权的指针(即std::unique_ptr或std::weak_ptr)出发构造一个shared_ptr
  3. 当std::shared_ptr构造函数使用裸指针作为实参来调用时

引用计数的存在还会带来一些性能开销,同时为了线程安全,引用计数的递增和递减是原子操作的。

把刚才的 unique_ptr 的代码改为 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;
};

对于weak_ptr,有 lock 方法可以返回一个 shared_ptr 指针,我们可以应用如下:

std::shared_ptr<Animal> CalAnimal(unsigned int animal_id)
{
	// 一堆计算
	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(); // 如果不在缓存中,则返回空指针

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

还是对于那个 Animal 的示例,假定有这么一个函数CalAnimal,传入一个id,经过复杂的计算决定要返回什么动物。但是要是传入相同的id,却要经过重复的计算,为了节约时间,我们用一个哈希表来存储要返回的结果。

这个存储不能是shared_ptr形式的,否则会导致一直存在引用计数,即使外面已经没有使用这个对象了,函数内部依然存储着;因此我们可以选用weak_ptr,如上代码。

仍然存在的一个问题就是会导致std::weak_ptr不断积累了。

std::bad_weak_ptr 异常

weak_ptr 的空悬(dangling pointer,空悬指针),也被叫做失效(expired),可以用 expired 方法测试:

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

要是直接用weak_ptr来作为实参构造shared_ptr,当weak_ptr失效的话就会抛出异常:

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

还记得我们之前说,当std::shared_ptr构造函数使用裸指针作为实参来调用时,会产生一个新的控制块。这就导致在类内部的方法中,将一个类的 this 指针去构造 shared_ptr 会产生问题——一个新的控制块!

比如如下代码:

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);
	}
};	

我们在使用:

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

这是一个未定义行为。因为 d 是一个 shared_ptr ,但其实它调用 PushBack 方法的时候由于是用this指针构造,则会导致push进去的 shared_ptr 指向的对象(托管的对象资源)和 d 是一个对象,可是实际上由于控制块的不同却是两个不同的 shared_ptr ;那么 d 析构的时候,对象被析构,而容器内的 shared_ptr 析构的时候,对象再次被析构,第二次析构就会导致未定义行为。

于是解决方法就可以用 std::enable_shared_from_this ,我们更改刚刚写的类如下:

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());
	}
};	

之后写代码:

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

就都没有问题了。

那么 std::enable_shared_from_this 是怎么实现的呢?

参考:https://zh.cppreference.com/w/cpp/memory/enable_shared_from_this

enable_shared_from_this 的常见实现为:其内部保存着一个对 this 的弱引用(例如 std::weak_ptr )。当调用 shared_from_this 方法的时候就会返回一个由该弱指针构造出来的shared_ptr:
在这里插入图片描述
保证弱指针是对this的弱引用的方法,就是CRTP了,这里类的模板参数是 _Ty:
在这里插入图片描述
弱指针指向的便是 _Ty:
在这里插入图片描述
于是通过 CRTP 的方法,这里的 _Ty 实际上就是 Animal 类了:
在这里插入图片描述
从而达成实现。

补充

swap 与 reset:

三个智能指针都有这两种方法。swap 交换,a.swap(b),reset 重置,稍微有些不同:
深入理解C++智能指针——浅析MSVC源码_第4张图片
深入理解C++智能指针——浅析MSVC源码_第5张图片
深入理解C++智能指针——浅析MSVC源码_第6张图片
可以看到只有weak_ptr是无参的,调用相当于直接释放(弱引用计数 - -)。其实现也简单的不行:
在这里插入图片描述

shared_ptr和weak_ptr还有use_count方法可以查看引用计数。

你可能感兴趣的:(c++,开发语言,后端)