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
在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
这样好像也没问题,因为shared_ptr有unique_ptr的构造函数:
std::shared_ptr
参考:
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
在我曾经的笔记曾提到过:https://zhuanlan.zhihu.com/p/415508858
参考链接:
https://en.cppreference.com/w/cpp/memory
https://en.cppreference.com/w/cpp/memory/shared_ptr
关于这两个,在MSVC的实现中会把他们继承于同一个基类(但是似乎标准规格书没有写,猜测是各家编译器自己实现的),这在前面讲unique_ptr的时候曾讲过:
std::remove_extent_t 确实是 C++14 引入的,至于前面那张图的 element_type 看来是在 C++17 才做出了这样的更新。
关键要理解的是,引用计数并不是类里面封装一个size_t一般类型的数,类里面实际封装的是一个指向一个控制块的指针(类 _Ptr_base 中):
第一个 element_type 标准已经讲了,第二个就是这个指向控制块的指针:
可以看到,其有一些虚的方法,还有两个计数:引用计数和弱计数。
既然 shared_ptr 与 weak_ptr 是继承同一个基类,那么配套的自然也就指向同一个控制块了。对于 std::make_shared
,其控制块和托管 element_type 类型的资源会一同分配,从而只要引用计数归0,就会一同析构(但若是对象很大,则很可能要到弱计数归0的时候才会被析构)。
那么什么时候会产生新的控制块呢?参考effective modern C++,在以下三种情况将会创建新的控制块:
引用计数的存在还会带来一些性能开销,同时为了线程安全,引用计数的递增和递减是原子操作的。
把刚才的 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
不断积累了。
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::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 重置,稍微有些不同:
可以看到只有weak_ptr是无参的,调用相当于直接释放(弱引用计数 - -)。其实现也简单的不行:
shared_ptr和weak_ptr还有use_count方法可以查看引用计数。