• Effective C++条款11:在operator=中处理“自我赋值”(Handle assignment to self in operator=)



    《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:


    条款11:在 operator= 中处理“自我赋值”

      “自我赋值”发生在对象被赋值给自己时:

    class Widget{};
     
    Widget w;
    w = w;  //自我赋值
    
    • 1
    • 2
    • 3
    • 4

      这看起来有点蠢,但它合法,所以不要认定客户绝不会这样做。此外赋值动作并不总是那么可被一眼辨识出来,例如:

    a[i] = a[j];
    
    • 1

      如果 a 是一个数组,且索引i和j相等,那么也是一个潜在的自我赋值。

    *px = *py;
    
    • 1

      如果这两个指针px和py指向于同一块内存,那么也是一个潜在的自我赋值

      这些并不明显的自我赋值,是“别名”带来的结果:所谓“别名”就是有一个以上的方法(指涉)某对象。

    类中自我赋值问题及如何解决

    假设你建立一个类Widget,用来保存一个指针指向一块动态分配的位图(bitmap):

    class Bitmap { ... };
    class Widget {
    	...
    private:
    	Bitmap* pb; // 指针,指向一个从 heap 分配而得的对象
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

      下面是operator=的实现代码,表面上看起来合理,但自我赋值出现时并不安全(也不具备异常安全性)

    class Bitmap {};
    class Widget {
    public:
        //赋值运算符
        Widget&
        Widget::operator=(const Widget& rhs) // 一份不安全的 operator= 实现版本
        {
            delete pb;
            pb = new Bitmap(*rhs.pb); //是使用rhs's bitmap的副本
            return *this;
        }
    private:
        Bitmap* pb;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

      这里自我赋值的问题是,operator=函数内的 *this (赋值的目的端)和 rhs 有可能是同一个对象,果真如此 delete 就不只是销毁当前对象的 bitmap,它也销毁 rhs 的 bitmap。在函数末尾,Wigdet——它原本不该被自我赋值动作改变的——发现自己持有一个指针指向一个已被删除的对象!

      现在来分析这个运算符如果出现自我赋值而产生的错误:

    • 如果参数rhs传入的就是自身,那么当pb被释放之后,下面再次new的时候又将参数(自己)的pb指针所指的内容传入进去,但是pb的内容已经被释放了,因此再次使用到这个对象的时候就会产生不确定的行为。

    错误解决:

      想要阻止这种错误,做法是在赋值运算符最前面的一个“认同测试”达到“自我赋值”的检验目的:
      根据上面“自我赋值”而产生的错误,我们应该在赋值运算符函数的第一步判断传入的对象是否为自己,如果为自己的话做相应的处理

    Widget& Widget::operator=(const Widget& rhs)
    {
        //判断是否为“自我赋值”
        //注意,此处的&为取地址
        if (this == &rhs)//测试
            return *this;
     
        //其他的与上面介绍的一样
        delete pb;
        pb = new Bitmap(*rhs.pb); //以参数为副本调用拷贝构造函数重新创建
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    异常处理

      上面介绍的operator=虽然解决了“自我赋值”检测,但是不是“异常安全的”。例如在new操作符执行时跑出了异常(内存不足或因为Bitmap类的拷贝构造函数抛出异常),最终Widget对象会只有一个一块已被删除的Bitmap,因此代码是不安全的。

      我们对上面代码进行优化:

    Widget& Widget::operator=(const Widget& rhs)
    {
        Bitmap *pOrig = pb; //记住原先的pb
        pb = new Bitmap(*rhs.pb); //以参数为副本让pb重新创建一个对象
        delete pOrig;  //删除原先的pb
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      这段代码可以来处理异常:如果new时抛出了异常,此时我们的pb对象还没有删除

      这段代码还可以来处理“自我赋值”:我们对原bitmap做了一份复制、删除原bitmap,然后将pb再指向于复制的那一份。这个虽然不是处理“自我赋值”最高效的办法,但是行得通。

      关于效率:此处为什么我们不在代码最前面进行“对象是否为自己”的检测了:此处我们的代码已经可以处理自我赋值了,如果还添加“三”中那种“自我检测”的代码,会使代码增多并多了一个语句判断,会使执行速度降低

    使用“copy and swap”技术来处理自我赋值

      替换上面的所有办法,我们可以使用“copy and swap”技术来解决“自我赋值”以及“异常处理”

      copy and swap技术和“异常安全性”有密切关系,会在条款29详细讲述

    手法1:

    class Bitmap {};
    class Widget {
    public:
        void swap(Widget& rhs); //将参数rhs与*this进行数据交换,详情见条款29
        Widget& Widget::operator=(const Widget& rhs)
        {
            Widget temp(rhs);//以函数参数为参数调用Wiget的拷贝构造函数创建一个对象
            //不能将rhs直接传入swap,因为这样的话会改变=号后面对象的内容,因此上面需要创建一个临时对象temp
            swap(temp);      //交换参数所指的对象与*this
            return *this;
        }
    private:
        Bitmap* pb;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    手法2:

      实现这种技术的手法还有一种是以“传值调用”operator=,实现如下:

      这种技术手法实现的功能与上面的一样,但是代码没有那么清晰

      但是这种手法将“拷贝”动作从函数体内移动到了“函数参数构造阶段”,因此效率提高了。

    class Bitmap {};
    class Widget {
    public:
        void swap(Widget& rhs); //将参数rhs与*this进行数据交换
        //rhs是被传对象的一份副本,这样的话我们就不用在operator=为参数创建一个临时对象了
        Widget& Widget::operator=(Widget rhs)
        {
            swap(rhs); //将传入的副本与*this进行数据替换
            return *this;
        }
    private:
        Bitmap* pb;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    总的来说:

    • 类的拷贝赋值操作符可能被声明“以 by value 方式接受实参”。
    • 以传值方式传递东西会生成一份副本。

    牢记

    • 确保当前对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy=and-swap

    • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确

    总结

    期待大家和我交流,留言或者私信,一起学习,一起进步!

  • 相关阅读:
    Allegro在板内添加器件限高区操作指导
    aws lakeformation工作流程和权限管理逻辑
    Haproxy集群
    2022-11-21 mysql列存储引擎-架构实现缺陷梳理-P2
    长春理工大学2013年全国硕士研究生统一入学考试自命题试题
    基于学生成绩管理系统(附源代码及数据库)
    git remote 使用方法
    LeetCode-104. Maximum Depth of Binary Tree [C++][Java]
    华为云云服务器评测 [Vue3 博物馆管理系统] 使用Vue3、Element-plus菜单组件构建轮播图
    C语言常考面试基础问题
  • 原文地址:https://blog.csdn.net/CltCj/article/details/127996221