|
在开始之前,不清楚智能指针的,可以先看一下这个
智能指针,本质上是对资源所有权和生命周期管理的抽象:
1. 当资源是被独占时,使用 std::unique_ptr 对资源进行管理。
2. 当资源会被共享时,使用 std::shared_ptr 对资源进行管理。
3. 使用 std::weak_ptr 作为 std::shared_ptr 管理对象的观察者。
4. 通过继承 std::enable_shared_from_this 来获取 this 的 std::shared_ptr 对象。
参考:FOCUS (2020), “现代 C++:一文读懂智能指针”, 知乎[Online]. Available: https://zhuanlan.zhihu.com/p/150555165, [Accessed At: 2022/10/01] 文中除了smart point之外,也会提到dump point,后者是指C++的内建指针。
本章节中,主要讲了智能指针通过定义以下几种行为来实现的
下面是几个需要注意的点
1.如果你的 smart pointer classes 不允许被复制或赋值,你应该将 copy constructor 和 assignment operator 声名为 private。
2.考虑删除对象
你可能有一个这样的智能指针
template<class T>
class auto_ptr{
public:
auto_ptr(T *ptr = 0):pointee(ptr) {};
~auto_ptr() { delete pointee };
...
private:
T *pointee;
}
考虑到这样的调用
auto_ptr<TreeNode> ptn1(new TreeNode);
auto_ptr<TreeNode> ptn2 = ptn1; // 调用 copy cotr
auto_ptr<TreeNode> ptn3;
ptn3 = pnt2; // 调用 operator=
这里会调用多次delete,从而造成灾难性的后果。
为了消除上述问题,我们可以禁止 auto_ptr 被复制和赋值。但是我们可以通过下面的议题来优化它。
3.转移对象拥有权
这个是一个更富弹性的解决方案,详细见代码
template<class T>
class auto_ptr{
public:
...
auto_ptr(auto_ptr<T> &rhs); // copy constructor
auto_ptr<T>& operator=(auto_ptr<T> &rhs); // assignment operator
...
}
template<class T>
auto_ptr<T>::auto_ptr(auto_ptr<T> &rhs)
{
pointee = rhs.pointee; // 将 *pointee 的拥有权转移至 *this。
rhs.pointee = 0; // rhs 不再拥有任何东西
}
template<class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T> &rhs)
{
if(this == &rhs) // 如果自己赋值给自己
return *this; // 不做任何事情
delete pointee; // 删除目前拥有之物
pointee = rhs.pointee; // 将 *pointee 的拥有权转移给 *this。
rhs.pointee = 0; // rhs 不再拥有任何东西
return *this;
}
4.by value 方式传递 auto_ptr 往往是个糟糕的主意
首先先看代码
void printTreeNode(ostream &s, auto_ptr<TreeNode> p)
{
s << *p;
}
int main()
{
auto_ptr<TreeNode> ptn(new TreeNode);
...
printTreeNode(cout, ptn); // 以 by value 方式传递 auto_ptr
...
}
当 printTreeNode 的参数 p 被初始化,ptn 所指对象的拥有权被转移至 p。当 printTreeNode 结束,p 即将离开其生存空间,于是其 destructor 会删除它所指之物。然而 ptn 不指向任何东西,所以 printTreeNode 被调用之后,任何人如果使用 ptn 都会导致未定义的行为。
这并不意味着你不能以 auto_ptrs 作为参数,这只是意味 pass-by-value 并不适用。 Pass-by-reference-to-const 才是适用的途径。
coid printTreeNode(ostream& s, const auto_ptr<TreeNode> &p)
{
s << *p;
}
5.解引问题
5.1.初版解引
template<class T>
T& SmartPtr<T>::operator*() const
{
// 处理智能指针需要做的事情
return *pointee;
}
template<class T>
T* SmartPtr<T>::operator->() const
{
// 处理智能指针需要做的事情
return pointee;
}
5.2.判断是否为null指针
SmartPtr<TreeNode> ptr;
...
if(ptn == 0) ... // 错误
if(ptn) ... // 错误
if(!ptn) ... // 错误
为了解决上面的问题,smart points 不能像 dump pinters 一样自然地调用,一种做法是提供一个隐式类型转换操作符。
template<class T>
class smartPtr{
public:
...
operator void*(); // 如果 dump ptr 是 null,返回0
...
};
SmartPtr<TreeNode> ptn;
...
if(ptn == 0) ... // 可以了
if(ptn) ... // 可以了
if(!ptn) ... // 可以了
但是带来的问题是
SmartPtr<Apple> pa;
SmartPtr<Orange> po;
...
if(pa == po) ... // 竟然可以过关!
如果不明白为什么,请折回去查看条款5 (【《More Effective C++ (35个改善编程与设计的有效方法)》 读书笔记】条款5:对定制的 “类型转换函数” 保持警觉)。
为了解决这个问题,另外一种解决方案是重载 ! 操作符,并通过使用 operator! 来判断是否为 null。
template<class T>
class SmartPtr {
public:
...
bool operator!() const;
...
};
SmartPtr<TreeNode> ptn;
if(!ptn) ... // 可以了
if(ptn == 0) ... // 还是不行
if(ptn) ... // 还是不行
同时我们也并不能解决之前提到的这个问题
SmartPtr<Apple> pa;
SmartPtr<Orange> po;
...
if(!pa == !po) ... // 这里还是可以通过编译
6.将 smart pointers 转换为 dumb pointers
假设我们有这样的代码
class TreeNode {...}; // 不用太在意里面有什么,只要知道是一个类就行
void PrintTree(TreeNode *pt); // 注意这里需要的实参是 TreeNode*
// 如果我们这样调用
SmartPtr<TreeNode> stn;
PrintTree(stn); // 错误
这里虽然说 stn 是个智能指针,但是却不能转换为 TreeNode 样式的指针。
虽然我们可以通过以下方法来调用,但是很丑陋。
PrintTree(&*pt);
为了解决上述的问题,我们需要为 smart point-to-T template 加上一个饮食类型转换操作符,使之可转换为 dumb pointer-to-T。
template<class T>
class smartPtr{
public:
...
operator T*() { rethrn pointee; } // 新增的抓换操作符
...
};
SmartPtr<TreeNode> stn;
PrintTree(stn); // 现在就可以了
上述的代码同时可以解决我们上面提到的 nullness 测试问题
if(ptn) ... // 可以了
if(!ptn) ... // 可以了
if(ptn == 0) ... // 可以了
但是这样的设计也有黑暗面,这样就回避了 smart pointer 设计的初衷,使得用户能够直接使用到 dump pointers。
同时,让我们考虑这样的代码
class TreeHolder{
public:
// 这里只需要关注我们后续可以使用 TreeNode* 隐式构造 TreeHolder
TreeHolder(const TreeNode *ptn);
...
// 这里提供一个函数,合并两个 TreeHolder
TreeHolder* merge(const TreeHolder &th1, const TreeHolder &th2);
}
// 先看一下自然的调用情况
TreeNode *ptn1, *ptn2;
TreeHolder *newTH = merge(ptn1, ptn2); // 没有问题,两个指针都会被转换为 TreeHolder
// 再看一下智能指针的调用情况
SmartPtr<TreeNode> sp1, sp2;
TreeHolder *newTH = merge(sp1, sp2); // 错误!无法将 sp1 和 sp2 转换为 TreeHolder 对象
在第二种情况下,SmartPtr<TreeNode> 转换为 TreeHolder 需要用户定制两个转换
- 将 SmartPtr<TreeNode> 转换为 TreeNode*
- 将 TreeNode* 转换为 TreeHolder
而上面的转换情节是C++所不允许的。
smart pointer classes 如果提供隐式转换至 dumb pointer,便是打开了一个难缠臭虫的门户。考虑以下代码
SmartPtr<TreeNode> sp = new TreeNode();
...
delete sp;
结论就是:不要提供对 dump pointers 的隐式转换操作符,除非不得已
7.Smart pointers 和 “与继承有关的”类型转换的问题

class MusicProduct{
public:
MusicProduct(const string& title);
virtual void play() const = 0;
virtual void displayTitle() const = 0;
...
}
class Cassette: public MusicProduct {
pubic:
Cassette(const string& title);
virtual void play() const;
virtual void displayTitle() const;
...
};
class CD: public MusicProduct {
pubic:
CD(const string& title);
virtual void play() const;
virtual void displayTitle() const;
}
并且我们有一个函数displayAndPlay,接受一个MusicProduct指针,显示标题并播放。
void displayAndPlay(const MusicProduct *pmp, int numTimes)
{
pmp->displayTitle();
pmp->play();
}
自然情况下,我们可以这样调用函数
Cassette *funMusic = new Cassette(&#34;Alpalooza&#34;);
CD *nightmareMusic = new CD(&#34;Disco Hits of the 70s&#34;);
displayAndPlay(funMusic);
displayAndPlay(nightmareMusic);
但是如果我们将 dumb pointers 以其 smart pointers 取代
void displayAndPlay(const SmartPtr<MusicProduct> &pmp);
SmartPtr<Cassette> funMusic(new Cassette(&#34;Alapalooza&#34;));
SmartPtr<CD> nightmareMusic(new CD(&#34;Disco Hits of the 70s&#34;));
displayAndPlay(funMusic); // 错误
displayAndPlay(nightmareMusic); // 错误
上面的代码没有办法通过编译,是因为没有办法将 SmartPtr<CD> 或 SmartPtr<Cassette> 转换为 SmartPtr<MusicProduct>.
7.1.一个简单的解决方案
class SmartPtr<Cassette> {
public:
operator SmartPtr<MusicProduct>() { return SmartPtr<MusicProduct>(pointee); }
...
private:
Cassette *pointee;
}
class SmartPtr<CD> {
public:
operator SmartPtr<MusicProduct>() { return SmartPtr<MusicProduct>(pointee); }
...
private:
CD *pointee;
}
此做法有两个缺点
- 你必须一一为每一个“SmartPtr class 实例” 加入上述特殊式子,如此一来才有机会加上必要的隐式类型转换操作符。
- 你可能必须加上许多如此这般的转换操作符,因为你所指的对象可能位于继承体系的底层,而你必须为对象直接继承或间接继承的每一个 base class 提供一个转换操作符。
为了解决这个问题,可以通过升级我们的 SmartPtr,把 nonvirtual memver function 声明为 templates,从而来产生 smart pointer 的转换函数。
template<class T> // template class,用于隐式转换操作符
class SmartPtr{
public:
SmartPtr(T* realPtr = 0);
T* operator->() const;
T& operator*() const;
template<class newType> // template function,用于隐式转换操作符。
operator SmartPtr<newType>() {
return SmartPtr<newType>(pointee);
}
...
}
这样一来
void displayAndPlay(const SmartPtr<MusicProduct> &pmp);
SmartPtr<Cassette> funMusic(new Cassette(&#34;Alapalooza&#34;));
SmartPtr<CD> nightmareMusic(new CD(&#34;Disco Hits of the 70s&#34;));
displayAndPlay(funMusic); // 成功了,原本是错误的
displayAndPlay(nightmareMusic); // 成功了,原本是错误的
7.2.缺点
考虑以下继承关系

此时我们有这样的代码
template<class T>
class SmartPtr { ... };
void displayAndPlay(const SmartPtr<MusicProduct> &pmp);
void displayAndPlay(const SmartPtr<Cassette> &pc);
SmartPtr<CasSingle> dumpbMusic(new Cassingle(&#34;Achy Breaky Heart&#34;));
displayAndPlay(dumpbMusic); // 错误!
在 displayAndPlay(dumpbMusic) 中,我们没有定义 void displayAndPlay(const SmartPtr<Cassingle> &pc); 函数,我们期望调用的是 void displayAndPlay(const SmartPtr<Cassette> &pc);。但是 SmarPtr 并不是那么智能,它们把 member functions 拿来作为转换操作符使用,而C++的理念是,**对于任何转换函数的调用动作,无分轩轾地好。于是 displayAndPlay 的调用动作成为一种模棱两可的行为,因为从 SmartPtr<Cassingle> 转换至 SmartPtr<Cassette> 并不比转换至 SmartPtr<MusicProduct> 好 **
利用 member templates 来转换 smart pointer 有另外两个缺点
- 目前支持 member templatees 的编译器还不多,此技术虽好,移植性不高
- 其间涵盖的技术并不那么简单,你必须熟悉
- 函数调用的自变量匹配规则
- 隐式类型转换函数
- template function 的暗自实例化
- member function templates
- ...
7.3.总结
如何能够使 “smart point classes 的行为”在“与继承相关的类型转换”上,能够和 dump pointers 一样?答案很简单:不能够。
8.Smart Pointers 与 const
思考一段代码
CD *pCD = new CD(&#34;Famouse Move Themes&#34;);
const CD *pCconstCD = pCD; // 可以
SmartPtr<CD> spCD = new CD(&#34;Famouse Movie Themes&#34;);
SmartPtr<const CD> spConstCD = spCD; // 不行
后者中, SmartPtr<CD> 和 SmartPtr<const CD> 是完全不同的类型。就目前的编译器而言,它们之间毫无广西,所以没有理由认为它们之间可以彼此赋值。唯一能够让两个类型被视为“课互相赋值”的情况是,如果你提供一个函数,能够将 SmartPtr<CD> 对象转换为 SmartPtr<const CD> 对象。
如果确实有需要,可以通过C语言的旧武器 union 获得解决。这一 union 的访问层级应该是 protected,使用两个 classes 都可取用。其中内含两个必要的 dumb pointer 类型:constPointee 指针提供 SmartPtrToConst<T> 对象使用,pointee 指针提供 SmartPtr<T> 对象使用。我们因此获得了两个互异指针的有点,又不必分配多余控件。
9.测试代码
这里总结了一些SmartPtr的代码,供参考
#include <iostream>
template<class T>
class SmartPtr {
public:
SmartPtr(T* p = 0)
:pointee(p) {}
T& operator*() {
// 处理智能指针需要做的事情
return &pointee;
}
T* operator->() {
// 处理智能指针需要做的事情
return pointee;
}
operator T* () {
std::cout << typeid(*this).name() << &#34;&#39;s T*&#34; << std::endl;
return pointee;
};
operator void* ()
{
std::cout << typeid(*this).name() << &#34;&#39;s operator void*&#34; << std::endl;
return pointee;
}
template<class newType>
operator SmartPtr<newType>() {
std::cout << &#34;触发了模板的转换:&#34; << typeid(*this).name() << &#34;&#39;s operator SmartPtr<&#34; << typeid(newType).name() << &#34;>()&#34; << std::endl;
return SmartPtr<newType>(pointee);
}
private:
T* pointee;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class Base {
public:
Base() {
std::cout << &#34;ctor Base.&#34; << std::endl;
}
};
class A : public Base {
public:
A() {
std::cout << &#34;ctor A.&#34; << std::endl;
}
};
class B : public Base {
public:
B() {
std::cout << &#34;ctor B.&#34; << std::endl;
}
};
class D : public B {
D() {
std::cout << &#34;ctor D.&#34; << std::endl;
}
};
void TestFunc(SmartPtr<Base> base) {
std::cout << &#34;call TestFunc: &#34; << typeid(*base).name() << &#34;&#39;s operator SmartPtr<Base>()&#34; << std::endl;
}
void TestFunc(SmartPtr<A> a) {
std::cout << &#34;call TestFunc: &#34; << typeid(*a).name() << &#34;&#39;s operator SmartPtr<A>()&#34; << std::endl;
}
void TestFunc(SmartPtr<B> b) {
std::cout << &#34;call TestFunc: &#34; << typeid(*b).name() << &#34;&#39;s operator SmartPtr<B>()&#34; << std::endl;
}
// 这里隐藏一下,为了测试之后继承方面的问题
// void TestBase(SmartPtr<D> d) {
// std::cout << typeid(*d).name() << &#34;&#39;s operator SmartPtr<D>()&#34; << std::endl;
// }
void TestBase(SmartPtr<Base> base) {
std::cout << &#34;call TestBase: &#34; << typeid(*base).name() << &#34;&#39;s operator SmartPtr<Base>()&#34; << std::endl;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int main() {
SmartPtr<Base> spBase;
SmartPtr<A> spA;
SmartPtr<B> spB;
// 如果这样写 if(spA) {} 编辑器会找到 SmartPtr<A> 中的
// 1)SmartPtr<T>::operator T *();
// 2)SmartPtr<T>::operator void *();
// 所以接下来的测试需要指明使用哪一个
// 这里我们强制使用 void *()
if ((void*)spA) // class SmartPtr<class A>&#39;s operator void*
std::cout << &#34;spA&#34; << std::endl;
else
std::cout << &#34;!spA&#34; << std::endl; // !spA
// 这里我们强制使用 T *()
if ((A*)spA) // class SmartPtr<class A>&#39;s T*
std::cout << &#34;spA&#34; << std::endl;
else
std::cout << &#34;!spA&#34; << std::endl; // !spA
// 这里会触发 void*
if (spA == spB) // class SmartPtr<class A>&#39;s operator void*
// class SmartPtr<class B>&#39;s operator void*
std::cout << &#34;spA == spB&#34; << std::endl; // spA == spB
else
std::cout << &#34;spA != spB&#34; << std::endl;
// 这里测试继承,TestFunc 函数需要传入一个 SmartPtr<Base> 的智能指针
TestFunc(spBase); // call TestFunc: class Base&#39;s operator SmartPtr<Base>()
TestFunc(spA); // call TestFunc: class Base&#39;s operator SmartPtr<Base>()
TestFunc(spB); // call TestFunc: class Base&#39;s operator SmartPtr<Base>()
SmartPtr<D> spD;
// 这里如果直接调用 TestBase(spD); 会报错
// 有多个重载函数 &#34;TestBase&#34; 实例与参数列表匹配:
// 函数 TestFunc(SmartPtr<Base> base)
// 函数 TestFunc(SmartPtr<A> a)
// 函数 TestFunc(SmartPtr<B> b)
// 参数类型为 (SmartPtr<D>)
// 因为我们上面注释了void TestFunc(SmartPtr<D> d)
// 如果我们只定义一个 TestBase(SmartPtr<Base> base) 函数,不重载。
TestBase(spBase); // call TestBase: class Base&#39;s operator SmartPtr<Base>()
TestBase(spA); // 触发了模板的转换:class SmartPtr<class A>&#39;s operator SmartPtr<class Base>()
// call TestBase: class Base&#39;s operator SmartPtr<Base>()
TestBase(spB); // 触发了模板的转换:class SmartPtr < class B>&#39;s operator SmartPtr<class Base>()
// call TestBase: class Base&#39;s operator SmartPtr<Base>()
TestBase(spD); // 触发了模板的转换:class SmartPtr<class D>&#39;s operator SmartPtr<class Base>()
// call TestBase: class Base&#39;s operator SmartPtr<Base>()
return 0;
}
<hr/>上一篇:【《More Effective C++ (35个改善编程与设计的有效方法)》 读书笔记】条款27:要求(或禁止)对象产生于 heap 中
下一篇:【《More Effective C++ (35个改善编程与设计的有效方法)》 读书笔记】条款29:Reference counting(引用计数) |
|