查看: 113|回复: 0

【《More Effective C++ (35个改善编程与设计的有效方法 ...

[复制链接]

1

主题

8

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2022-12-3 19:24:34 | 显示全部楼层 |阅读模式
在开始之前,不清楚智能指针的,可以先看一下这个
智能指针,本质上是对资源所有权和生命周期管理的抽象:
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("Alpalooza");
CD *nightmareMusic = new CD("Disco Hits of the 70s");

displayAndPlay(funMusic);
displayAndPlay(nightmareMusic);
但是如果我们将 dumb pointers 以其 smart pointers 取代
void displayAndPlay(const SmartPtr<MusicProduct> &pmp);

SmartPtr<Cassette> funMusic(new Cassette("Alapalooza"));
SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s"));

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("Alapalooza"));
SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s"));

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("Achy Breaky Heart"));
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("Famouse Move Themes");
const CD *pCconstCD = pCD;                  // 可以

SmartPtr<CD> spCD = new CD("Famouse Movie Themes");
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() << "'s T*" << std::endl;
        return pointee;
    };

    operator void* ()
    {
        std::cout << typeid(*this).name() << "'s operator void*" << std::endl;
        return pointee;
    }

    template<class newType>
    operator SmartPtr<newType>() {
        std::cout << "触发了模板的转换:" << typeid(*this).name() << "'s operator SmartPtr<" << typeid(newType).name() << ">()" << std::endl;
        return SmartPtr<newType>(pointee);
    }

private:
    T* pointee;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class Base {
public:
    Base() {
        std::cout << "ctor Base." << std::endl;
    }
};

class A : public Base {
public:
    A() {
        std::cout << "ctor A." << std::endl;
    }
};

class B : public Base {
public:
    B() {
        std::cout << "ctor B." << std::endl;
    }
};

class D : public B {
    D() {
        std::cout << "ctor D." << std::endl;
    }
};

void TestFunc(SmartPtr<Base> base) {
    std::cout << "call TestFunc: " << typeid(*base).name() << "'s operator SmartPtr<Base>()" << std::endl;
}

void TestFunc(SmartPtr<A> a) {
    std::cout << "call TestFunc: " << typeid(*a).name() << "'s operator SmartPtr<A>()" << std::endl;
}

void TestFunc(SmartPtr<B> b) {
   std::cout << "call TestFunc: " << typeid(*b).name() << "'s operator SmartPtr<B>()" << std::endl;
}

// 这里隐藏一下,为了测试之后继承方面的问题
// void TestBase(SmartPtr<D> d) {
//     std::cout << typeid(*d).name() << "'s operator SmartPtr<D>()" << std::endl;
// }

void TestBase(SmartPtr<Base> base) {
    std::cout << "call TestBase: " << typeid(*base).name() << "'s operator SmartPtr<Base>()" << 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>'s operator void*
        std::cout << "spA" << std::endl;
    else
        std::cout << "!spA" << std::endl;       // !spA

    // 这里我们强制使用 T *()
    if ((A*)spA)                                // class SmartPtr<class A>'s T*
        std::cout << "spA" << std::endl;
    else
        std::cout << "!spA" << std::endl;       // !spA

    // 这里会触发 void*
    if (spA == spB)                             // class SmartPtr<class A>'s operator void*
                                                // class SmartPtr<class B>'s operator void*
        std::cout << "spA == spB" << std::endl; // spA == spB
    else
        std::cout << "spA != spB" << std::endl;
   
    // 这里测试继承,TestFunc 函数需要传入一个 SmartPtr<Base> 的智能指针
    TestFunc(spBase);                           // call TestFunc: class Base's operator SmartPtr<Base>()
    TestFunc(spA);                              // call TestFunc: class Base's operator SmartPtr<Base>()
    TestFunc(spB);                              // call TestFunc: class Base's operator SmartPtr<Base>()

    SmartPtr<D> spD;  
    // 这里如果直接调用 TestBase(spD); 会报错
    // 有多个重载函数 "TestBase" 实例与参数列表匹配:
    //     函数 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's operator SmartPtr<Base>()           
    TestBase(spA);                              // 触发了模板的转换:class SmartPtr<class A>'s operator SmartPtr<class Base>()
                                                // call TestBase: class Base's operator SmartPtr<Base>()
    TestBase(spB);                              // 触发了模板的转换:class SmartPtr < class B>'s operator SmartPtr<class Base>()
                                                // call TestBase: class Base's operator SmartPtr<Base>()
    TestBase(spD);                              // 触发了模板的转换:class SmartPtr<class D>'s operator SmartPtr<class Base>()
                                                // call TestBase: class Base's operator SmartPtr<Base>()
    return 0;
}
<hr/>上一篇:【《More Effective C++ (35个改善编程与设计的有效方法)》 读书笔记】条款27:要求(或禁止)对象产生于 heap 中
下一篇:【《More Effective C++ (35个改善编程与设计的有效方法)》 读书笔记】条款29:Reference counting(引用计数)
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表