Guru of the Week 条款23:对象的生存期(第二部分)

翻译文档 专栏收录该内容
74 篇文章 0 订阅

GotW #23 Object Lifetimes – Part II

著者:Herb Sutter

翻译:CAT*G

[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者CAT*G在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者CAT*G对违反上述两条原则的人不负任何责任。特此声明。

Revision 1.0

 

Guru of the Week 条款23:对象的生存期(第二部分)

 

难度:6 / 10

 

(接着条款22,本期条款考虑一个经常被推荐使用的C++惯用法——它经常也是危险且错误的。)

 

[Problem]

[问题]

 

评述下面的惯用法(用常见的代码形式表达如下): 

    T& T::operator=( const T& other ) {
   
        if( this != &other ) {
   
            this->~T();
   
            new (this) T(other);
   
        }
   
        return *this;
   
    }
   

1.代码试图达到什么样的合法目的?修正上述代码中所有的编码缺陷。

 

2 .假如修正了所有的缺陷,这种惯用法是安全的吗?对你的回答做出解释。如果其是不安全的,程序员又该如何达到预想的目标呢?

 

(参见GotW条款22,以及October 1997 C++ Report

 

[Solution]

[解答]

 

评述下面的惯用法(用常见的代码形式表达如下):

    T& T::operator=( const T& other ) {
   
        if( this != &other ) {
   
            this->~T();
   
            new (this) T(other);
   
        }
   
        return *this;
   
}
   
 
   

[Summary][1]

[小结][1]

 

这个惯用法经常被推荐使用,且在C++标准草案中作为一个例子出现。[2]但其却具有不良的形式,而且——若要这么形容的话——恰恰是有害无益。请不要这样做。

 

1.代码试图达到什么样的合法目的?

 

这个惯用法以拷贝构造(copy construction)操作来实现拷贝赋值(copy assignment)操作。这即是说,该方法试图保证「T的拷贝构造与拷贝赋值实现的是相同的操作」,以避免程序员被迫在两个地方不必要的重复相同的代码。

 

这是一个高尚的目标。无论如何,它使编程更为简单,因为你不必把同一段代码编写两次,而且当T被改变(例如,给T增加了新的成员变量)的时候,你也不会像以前那样在更新了其中一个之后忘记更新另一个。

 

假如虚拟基类拥有数据成员,那么这个惯用法还是蛮有用的,因为若不使用此方法的话,数据成员在最好的情况下会被赋值数次,而在最坏的情况下则会被施以不正确的赋值操作。这听起来颇佳,但实际上却并无多大用处,因为虚拟基类其实是不应该拥有数据成员的。[3] 另外,既然有虚拟基类,那便意味着该类是为了用于继承而设计的——这又意味着:(正如我们即将看到的那样)我们不能使用这个惯用法,原因是它太具危险性。

 

修正上述代码中所有的编码缺陷。

 

上面的代码中包含一个可以修正的缺陷,以及若干个无法修正的缺陷。

 

[Problem #1: It Can Slice Objects]

[问题#1:它会切割对象]

 

如果T是一个带有虚拟析构函数(virtual destructor)的基类,那么”this->~T();”这一句就执行了错误的操作。如果是对一个派生类的对象执行这个调用的话,这一句的执行将会销毁派生类的对象并用一个T对象替代。而这种结果几乎将肯定破坏性的影响后续任何试图使用这个对象的代码。(更多关于“切割(slicing)” 问题的讨论,参见GotW 条款22

 

特别要指出的是,这种状况将会使编写派生类的编码者们陷入人间地狱般的生活(另外还有其它一些关于派生类的潜在陷阱,见下面的叙述)。回想一下,派生的赋值运算符通常是基于基类的赋值操作编写的: 

    Derived&
   
    Derived::operator=( const Derived& other ) {
   
        Base::operator=( other );
   
        // ...现在对派生成员进行赋值...
   
        return *this;
   
    }

这样我们得到: 

    class U : /* ... */ T { /* ... */ }; 
   
    U& U::operator=( const U& other ) {
   
        T::operator=( other );
   
        // ...现在对U 成员进行赋值... 呜呼呀
    
        return *this;           //呜呼呀
    
    }
   

正如代码所示,对T::operator=()的调用一声不响的对其后所有的代码(包括U成员的赋值操作以及返回语句)产生了破坏性的影响。如果U的析构函数没有把它的数据成员重置为无效数值的话(译注:即可以编译运行通过),这里将表现为一个神秘的、极难调试的运行期错误。

 

为了改正这个问题,可以调用"this->T::~T();"作为替代,这可以保证「对于一个派生类对象,只有其中的T subobject 被替换(而不是整个派生类对象被切割从而被错误的转为一个T对象)」。这样做只是用一个更为微妙的危险替换掉了一个明显的危险,而这个替换方案仍然会影响派生类的编写者(见下面的叙述)。

 

2.假如修正了所有的缺陷,这种惯用法是安全的吗?

 

不,不安全。要注意:如果不放弃整个惯用法的话,下列任何一个问题都无法得到解决:

 

[Problem #2: It's Not Exception-Safe]

[问题#2:它不是异常安全的]

 

‘new’语句将会唤起T的拷贝构造函数。如果这个构造函数可以抛出异常的话(其实许多甚至是绝大部分的类都会通过抛出异常来报告构造函数的错误),那么这个函数就不是异常安全的,因为其在构造函数抛出异常时会导致「销毁了原有对象而没有用新的对象替换上去」的情形。

 

与切割(slicing)问题一样,这个缺陷将会对后续的任何试图使用这个对象的代码产生破坏性影响。更糟糕的是,这还可能导致「程序试图将同一个对象销毁两次」的情况发生,因为外部的代码无法知晓这个对象的析构函数是否已经被运行过了。(参见GotW条款22中更多关于重复析构的讨论。)

 

[Problem #3: It’s Inefficient for Assignment]

[问题#3:它使赋值操作变得低效]

 

这个惯用法是低效的,因为赋值过程中的构造操作几乎总是涉及到比重置数值更多的工作。析构和重构在一起进行则更是增加了工作量。

 

[Problem #4: It Changes Normal Object Lifetimes]

[问题#4:它改变了正常的对象生存期]

 

这个惯用法破坏性的影响了那些依赖于正常的对象生存期之代码。特别是它破坏或干预了所有使用常见的“初始化就是资源获取(initialization is resource acquisition)”惯用法的类。

 

例如,若T在构造函数里获取了一个互斥锁(mutext lock)或者开启了数据库事务(database transaction),又在析构函数里释放这个锁或者事务处理,那会发生什么呢?这个锁或者事务处理将会以不正确的方式被释放并在赋值操作中被重新获得——这一般来说会破坏性的影响客户代码(client code)和这个类本身。除了TT的基类以外,如果T的派生类也依赖于T正常的生存期语义,它也会同样破坏性的影响这些派生类。

 

有人会说,“我当然决不会对一个在构造函数和析构函数中获取和释放互斥量的类使用这个惯用法了!”回答很简单:“真的吗?你怎么知道你使用的那些(直接或间接)的基类不这样做呢?”坦白的说,你经常是无法知晓这个情况的,你也绝不应该依赖那些工作起来似乎正常但却与对象生存期玩花招儿的基类。

 

这个惯用法的根本问题在于它搅浊了构造操作和析构操作的含义。构造操作和析构操作分别准确的对应于对象生存期的开始和结束,对象通常分别在这两个时刻获取和释放资源。构造操作和析构操作不是用来改变对象值的操作(实际上它们压根儿也不会改变对象的值,它们只是销毁原来的对象并替换上一个看起来一样、恰好拥有新数值的东西,其实这个新的东西与原来的对象根本就不是一回事儿)。

 

[Problem #5: It Can Still Break Derived Classes]

[问题#5:它可以对派生类产生破坏性影响]

 

"this->T::~T();"作为替代语句解决了问题#1之后,这个惯用法仅仅替换掉派生类对象中的T subobject。许多派生类都可以如此正常工作,把它们的基类subobject换出换入,但有些派生类却可能不行。

 

特别要指出的是,有些派生类可对其基类的状态予以控制,如果在不知道此信息的情况下对这些派生类的基类subobject进行盲目修改(以不可见的方式销毁和重构一个对象当然也算作是一种修改),那么这些派生类就可能导致产生失败。一旦赋值操作做了任何超出「一个“正常写入”型赋值运算符所应该做的操作」之额外操作,这个危险就会体现出来。

 

[Problem #6: It Relies on Unreliable Pointer Comparisons]

[问题#6:它依赖于不可靠的指针比较操作]

 

该惯用法完全依赖于"this != &other"测试。(如果你对此有疑问的话,请考虑自赋值的情形。)

 

其问题在于:这个测试并不保证你希望它保证的事情:C++标准保证「对指向同一个对象的多个指针的比较之结果必须是“相等(equal)” 」,但却并不保证「对指向不同对象的多个指针的比较之结果必须是“不相等(unequal)”」。如果这种情况发生,那么赋值操作就无法如愿完成。(关于"this != &other"测试的内容,参见GotW条款11。)

 

如果有人认为这太钻牛角尖了,请参看GotW条款11中的简要论述:所有“必须”检查自赋值(self-assignment)的拷贝赋值操作都不是异常安全的。[4][注意:请看Exceptional C++及其勘误表以得到更新的信息。]

 

另外还有一些能够影响客户代码和/或派生类的潜在危险(诸如虚拟赋值运算符的情形——这即使是在最好的情况下也还是多少有些诡异的),但到目前为止已经有足够多的内容用来演示该惯用法存在的严重问题了。

 

[So What Should We Do Instead?]

[那现在我们应该怎么做呢]

 

如果其是不安全的,程序员又该如何达到预想的目标呢?

 

用同一个成员函数完成两种拷贝操作(拷贝构造和拷贝赋值)的注意是很好的:这意味着我们只需在一个地方编写和维护操作代码。本条款问题中的惯用法只不过是选择了错误的函数来做这件事。如此而已。

 

其实,惯用法应该是反过来实现的:拷贝构造操作应该以拷贝赋值操作来实现,不是反过来实现。例如: 

    T::T( const T& other ) {
   
      /* T:: */ operator=( other );
   
    } 
   
    T& T::operator=( const T& other ) {
   
      // 真正的工作在这里进行
    
      // (大概可以在异常安全的状态下完成,但现在
    
      // 其可以抛出异常,却不会像原来那样产生什么不良影响
    
      return *this;
   
    }
   

这段代码拥有原惯用法的所有益处,却不存在任何原惯用法中存在的问题。[5] 为了代码的美观,你或许还要编写一个常见的私有辅助函数,利用其做真正的工作;但这也是一样的,无甚区别: 

    T::T( const T& other ) {
   
      do_copy( other );
   
    } 
   
    T& T::operator=( const T& other ) {
   
      do_copy( other );
   
      return *this;
   
    } 
   
    T& T::do_copy( const T& other ) {
   
      // 真正的工作在这里进行
    
      // (大概可以在异常安全的状态下完成,但现在
    
      // 其可以抛出异常,却不会像原来那样产生什么不良影响
    
}
   
 
   

[Conclusion]

[结论]

 

原始的惯用法中充满了缺陷,且经常是错误的,它使派生类的编写者过上人间地狱般的生活。我时常禁不住想把这个原始的惯用法贴在办公室的厨房里,并注明:“有暴龙出没。”

 

摘自GotW编码标准:

 

-如果需要的话,请编写一个私有函数来使拷贝操作和拷贝赋值共享代码;千万不要利用「使用显式的析构函数并且后跟一个placement new」的方法来达到「以拷贝构造操作实现拷贝赋值操作」这样的目的,即使这个所谓的技巧会每隔三个月就在新闻组中出现几次。(也就是说,决不要编写如下的代码:) 

        T& T::operator=( const T& other )
   
        {
   
            if( this != &other)
   
            {
   
                this->~T();             // 有害!
    
                new (this) T( other );  // 有害!
    
            }
   
            return *this;
   
        }
   

 

[Notes]

 

[1]:这里我忽略一些变态的情形(例如,重载T::operator&(),使其做出返回this以外的事情)。GotW条款11提到一些有关情况。

 

[2]:在C++标准草案中的那个例子意在演示对象生存期的规则,而不是要推荐一个好的现实用法(它不现实!)。下面给出草案3.8/7中的那个例子(处于空间的考虑做了微小的修改)以飨感兴趣的读者: 

  [例子:
   
    struct C {
   
      int i;
   
      void f();
   
      const C& operator=( const C& );
   
    };
   
    const C& C::operator=( const C& other)
   
    {
   
      if ( this != &other )
   
      {
   
        this->~C();     // '*this'的生存期结束
    
        new (this) C(other);
   
                        // 新的C型别的对象被创建
    
        f();            // 此处定义良好
    
      }
   
      return *this;
   
    }
   
    C c1;
   
    C c2;
   
    c1 = c2; //此处定义良好
    
    c1.f();  //此处定义良好; c1指的是
    
             //  新的C型别的对象
    
  --例子 ]

并不推荐实际使用该代码的进一步的证据在于:C::operator=()返回了一个const C&而不单纯是C&,这不必要的避免了这些对象在标准程序库之容器(container)中的可移植用法。

 

摘自GotW编码标准:

 

- 将拷贝赋值操作声明为 "T& T::operator=(const T&)"

- 不要返回const T&,尽管这样做避免了诸如"(a=b)=c"的用法;这样做意味着:你无法出于移植性的考虑而将T对象放入标准程序库之容器——因为其需要赋值操作返回一个单纯的T&Cline95: 212; Murray93: 32-33

 

[3]:参见Scott Meyers的《Effective C++

 

[4]:尽管你不能依赖于"this != &other"测试,但如果你为了通过优化处理排除已知的自赋值情形而这样做,则并没有错。如果它起作用的话,你便可以省掉一个赋值操作。当然,如果它不起作用的话,你的赋值运算符应该仍然以「对于自赋值而言是安全的」之方式来编写。关于使用这个测试作为优化手段,有人赞同也有人反对——但这超出了本期GotW的讨论范围。

 

[注5]:的确,它仍然需要一个缺省的构造函数,并可能仍然不是最高效的;但要知道,你唯有利用初始化列表(initializer lists)才能得到最优的高效性(利用初始化列表即在构造过程中同时初始化成员变量,一气呵成,而不是分为先构造,再赋值两步来完成)。当然,这样做又要牺牲代码的公用性(commonality),而对此的权衡也超出了本期GotW的讨论范围。

(完)

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
<p> <span style="font-size:14px;color:#337FE5;">【为什么学爬虫?】</span> </p> <p> <span style="font-size:14px;">       1、爬虫入手容易,但是深入较难,如何写出高效率爬虫,如何写出灵活性高可扩展爬虫都是一项技术活。另外在爬虫过程中,经常容易遇到被反爬虫,比如字体反爬、IP识别、验证码等,如何层层攻克难点拿到想要数据,这门课程,你都能学到!</span> </p> <p> <span style="font-size:14px;">       2、如果是作为一个其他行业开发者,比如app开发,web开发,学习爬虫能让你加强对技术认知,能够开发出更加安全软件和网站</span> </p> <p> <br /> </p> <span style="font-size:14px;color:#337FE5;">【课程设计】</span> <p class="ql-long-10663260"> <span> </span> </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 一个完整爬虫程序,无论大小,总体来说可以分成三个步骤,分别是 </p> <ol> <li class="" style="font-size:11pt;color:#494949;"> 网络请求模拟浏览器行为从网上抓取数据。 </li> <li class="" style="font-size:11pt;color:#494949;"> 数据解析将请求下来数据进行过滤,提取我们想要数据。 </li> <li class="" style="font-size:11pt;color:#494949;"> 数据存储将提取到数据存储到硬盘或者内存中。比如用mysql数据库或者redis等。 </li> </ol> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 那么本课程也是按照这几个步骤循序渐进进行讲解,带领学生完整掌握每个步骤技术。另外,因为爬虫多样性,在爬取过程中可能会发生被反爬、效率低下等。因此我们又增加了两个章节用来提高爬虫程序灵活性,分别是 </p> <ol> <li class="" style="font-size:11pt;color:#494949;"> 爬虫进阶包括IP代理,多线程爬虫,图形验证码识别、JS加密解密、动态网页爬虫、字体反爬识别等。 </li> <li class="" style="font-size:11pt;color:#494949;"> Scrapy和分布式爬虫Scrapy框架、Scrapy-redis组件、分布式爬虫等。 </li> </ol> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 通过爬虫进阶知识点我们能应付大量反爬网站,而Scrapy框架作为一个专业爬虫框架,使用他可以快速提高我们编写爬虫程序效率和速度。另外如果一台机器不能满足你需求,我们可以用分布式爬虫让多台机器帮助你快速爬取数据。 </p> <p style="font-size:11pt;color:#494949;">   </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 从基础爬虫到商业化应用爬虫,本套课程满足您所有需求! </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> <br /> </p> <p> <br /> </p> <p> <span style="font-size:14px;background-color:#FFFFFF;color:#337FE5;">【课程服务】</span> </p> <p> <span style="font-size:14px;">专属付费社群+定期答疑</span> </p> <p> <br /> </p> <p class="ql-long-24357476"> <span style="font-size:16px;"><br /> </span> </p> <p> <br /> </p> <p class="ql-long-24357476"> <span style="font-size:16px;"></span> </p>
<div style="color:rgba(0,0,0,.75);"> <span style="color:#4d4d4d;"> </span> <div style="color:rgba(0,0,0,.75);"> <span style="color:#4d4d4d;"> </span> <div style="color:rgba(0,0,0,.75);"> <div style="color:rgba(0,0,0,.75);"> <span style="color:#4d4d4d;">当前课程中商城项目实战源码是我发布在 GitHub 上开源项目 newbee-mall 新蜂商城,目前已有 6300 多个 star,</span><span style="color:#4d4d4d;">本课程是一个 Spring Boot 技术栈实战类课程,课程共分为 3 大部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 商城项目功能讲解,让大家实际操作并实践上手一个大型线上商城项目,并学习到一定开发经验以及其中开发技巧。<br /> 商城项目所涉及功能结构图整理如下<br /> </span> </div> <div style="color:rgba(0,0,0,.75);">   </div> <div style="color:rgba(0,0,0,.75);"> <p style="color:#4d4d4d;"> <img alt="modules" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3N0b3JlL25ld2JlZS1tYWxsLXMucG5n?x-oss-process=image/format,png" /> </p> </div> <p style="color:rgba(0,0,0,.75);"> <strong><span style="color:#e53333;">课程特色</span></strong> </p> <p style="color:rgba(0,0,0,.75);">   </p> <div style="color:rgba(0,0,0,.75);">   </div> <div style="color:rgba(0,0,0,.75);"> <ul> <li> 对新手开发者十分友好,无需复杂操作步骤,仅需 2 秒就可以启动这个完整商城项目 </li> <li> 最终实战项目是一个企业级别 Spring Boot 大型项目,对于各个阶段 Java 开发者都是极佳选择 </li> <li> 实践项目页面美观且实用,交互效果完美 </li> <li> 教程详细开发教程详细完整、文档资源齐全 </li> <li> 代码+讲解+演示网站全方位保证,向 Hello World 教程说拜拜 </li> <li> 技术栈新颖且知识点丰富,学习后可以提升大家对于知识理解和掌握,可以进一步提升你市场竞争力 </li> </ul> </div> <p style="color:rgba(0,0,0,.75);">   </p> <p style="color:rgba(0,0,0,.75);"> <span style="color:#e53333;">课程预览</span> </p> <p style="color:rgba(0,0,0,.75);">   </p> <div style="color:rgba(0,0,0,.75);">   </div> <div style="color:rgba(0,0,0,.75);"> <p style="color:#4d4d4d;"> 以下为商城项目页面和功能展示,分别为 </p> </div> <div style="color:rgba(0,0,0,.75);"> <ul> <li> 商城首页 1<br /> <img alt="" src="https://img-bss.csdnimg.cn/202103050347585499.gif" /> </li> <li> 商城首页 2<br /> <img alt="" src="https://img-bss.csdn.net/202005181054413605.png" /> </li> <li>   </li> <li> 购物车<br /> <img alt="cart" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3QvY2FydC5wbmc?x-oss-process=image/format,png" /> </li> <li> 订单结算<br /> <img alt="settle" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3Qvc2V0dGxlLnBuZw?x-oss-process=image/format,png" /> </li> <li> 订单列表<br /> <img alt="orders" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3Qvb3JkZXJzLnBuZw?x-oss-process=image/format,png" /> </li> <li> 支付页面<br /> <img alt="" src="https://img-bss.csdn.net/201909280301493716.jpg" /> </li> <li> 后台管理系统登录页<br /> <img alt="login" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3QvbWFuYWdlLWxvZ2luLnBuZw?x-oss-process=image/format,png" /> </li> <li> 商品管理<br /> <img alt="goods" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3QvbWFuYWdlLWdvb2RzLnBuZw?x-oss-process=image/format,png" /> </li> <li> 商品编辑<br /> <img alt="" src="https://img-bss.csdnimg.cn/202103050348242799.png" /> </li> </ul> </div> </div> </div> </div>
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值