【问题标题】:virtual assignment operator C++虚拟赋值运算符 C++
【发布时间】:2010-10-14 18:21:20
【问题描述】:

C++ 中的赋值运算符可以是虚拟的。为什么需要它?我们可以让其他运营商也虚拟化吗?

【问题讨论】:

    标签: c++ operator-overloading virtual virtual-functions


    【解决方案1】:

    赋值运算符不需要虚拟化。

    下面的讨论是关于operator=,但它也适用于接受相关类型的任何运算符重载,以及接受相关类型的任何函数。

    下面的讨论表明,在查找匹配的函数签名方面,virtual 关键字不知道参数的继承。在最后一个示例中,它展示了在处理继承类型时如何正确处理赋值。


    虚函数不知道参数的继承:

    一个函数的签名需要相同,虚拟才能发挥作用。因此,即使在下面的示例中,operator= 被设置为 virtual,调用也不会在 D 中充当虚拟函数,因为 operator= 的参数和返回值是不同的。

    函数B::operator=(const B& right)D::operator=(const D& right) 100% 完全不同,被视为两个不同的函数。

    class B
    {
    public:
      virtual B& operator=(const B& right)
      {
        x = right.x;
        return *this;
      }
    
      int x;
    
    };
    
    class D : public B
    {
    public:
      virtual D& operator=(const D& right)
      {
        x = right.x;
        y = right.y;
        return *this;
      }
      int y;
    };
    

    默认值和 2 个重载运算符:

    您可以定义一个虚函数,以便在将 D 分配给 B 类型的变量时为 D 设置默认值。即使您的 B 变量实际上是存储在 B 的引用中的 D。您将没有得到D::operator=(const D& right) 函数。

    在以下情况下,来自存储在 2 B 引用中的 2 D 对象的分配...使用 D::operator=(const B& right) 覆盖。

    //Use same B as above
    
    class D : public B
    {
    public:
      virtual D& operator=(const D& right)
      {
        x = right.x;
        y = right.y;
        return *this;
      }
    
    
      virtual B& operator=(const B& right)
      {
        x = right.x;
        y = 13;//Default value
        return *this;
      }
    
      int y;
    };
    
    
    int main(int argc, char **argv) 
    {
      D d1;
      B &b1 = d1;
      d1.x = 99;
      d1.y = 100;
      printf("d1.x d1.y %i %i\n", d1.x, d1.y);
    
      D d2;
      B &b2 = d2;
      b2 = b1;
      printf("d2.x d2.y %i %i\n", d2.x, d2.y);
      return 0;
    }
    

    打印:

    d1.x d1.y 99 100
    d2.x d2.y 99 13
    

    这表明从未使用过D::operator=(const D& right)

    如果没有B::operator=(const B& right) 上的 virtual 关键字,您将获得与上述相同的结果,但不会初始化 y 的值。 IE。它将使用B::operator=(const B& right)


    最后一步,RTTI:

    您可以使用 RTTI 正确处理接受您的类型的虚函数。这是解决在处理可能继承的类型时如何正确处理赋值的最后一块难题。

    virtual B& operator=(const B& right)
    {
      const D *pD = dynamic_cast<const D*>(&right);
      if(pD)
      {
        x = pD->x;
        y = pD->y;
      }
      else
      {
        x = right.x;
        y = 13;//default value
      }
    
      return *this;
    }
    

    【讨论】:

    • Brian,我发现这个问题中有一些奇怪的行为:stackoverflow.com/questions/969232/…。你有什么想法吗?
    • 我理解你关于使用 virtual 的论点,但在你的最后一篇文章中你使用了 'const D *pD = dynamic_cast(&right);',这似乎不正确放入基类。你能解释一下吗?
    • @Jake88:这不在基类中。它在派生类对基类中首先声明的虚拟 operator= 的覆盖中。
    • 消除歧义的最简单方法是将派生类的复制赋值运算符标记为“覆盖”,然后代码将无法编译,这证明了您对 2 个运算符的猜测(= 来自基础和派生)是不同的:类派生:public Base{ Derived& operator=(const Derived&)override{return *this;}};现在 Derived' = 运算符导致编译器在其基中搜索相应的成员,当然它会失败并产生错误。
    • 虽然我们可以多态地使用 =,但这没有意义,因为派生类版本必须具有相同的签名,这意味着它应该引用基而不是派生: struct D : B{D& operator=(const B&)override{return *this;}};虽然它可以编译,但它需要将对基的引用转换为派生的。
    【解决方案2】:

    这取决于运营商。

    将赋值运算符设为虚拟的目的是让您从能够覆盖它以复制更多字段的好处中受益。

    所以如果你有一个 Base& 并且你实际上有一个 Derived& 作为动态类型,并且 Derived 有更多的字段,那么正确的东西就会被复制。

    但是,您的 LHS 是 Derived,而 RHS 是 Base,因此存在风险,因此当虚拟运算符在 Derived 中运行时,您的参数不是 Derived,您无法从中获取字段。

    这是一个很好的讨论: http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html

    【讨论】:

      【解决方案3】:

      Brian R. Bondy 写道:


      最后一步,RTTI:

      您可以使用 RTTI 正确处理接受您的类型的虚函数。这是解决在处理可能继承的类型时如何正确处理赋值的最后一块难题。

      virtual B& operator=(const B& right)
      {
        const D *pD = dynamic_cast<const D*>(&right);
        if(pD)
        {
          x = pD->x;
          y = pD->y;
        }
        else
        {
          x = right.x;
          y = 13;//default value
        }
      
        return *this;
      }
      

      我想在这个解决方案中添加一些评论。将赋值运算符声明为与上面相同的三个问题。

      编译器生成一个赋值运算符,它接受一个 const D& 参数,该参数不是虚拟的,并且不会执行您可能认为的操作。

      第二个问题是返回类型,您正在返回对派生实例的基本引用。可能不是什么大问题,因为代码无论如何都可以工作。最好还是相应地返回引用。

      第三个问题,派生类型赋值运算符不调用基类赋值运算符(如果有你想复制的私有字段怎么办?),将赋值运算符声明为虚拟不会让编译器为你生成一个。这是一个副作用,因为没有至少两个赋值运算符重载来获得想要的结果。

      考虑基类(与我引用的帖子中的基类相同):

      class B
      {
      public:
          virtual B& operator=(const B& right)
          {
              x = right.x;
              return *this;
          }
      
          int x;
      };
      

      以下代码完成了我引用的 RTTI 解决方案:

      class D : public B{
      public:
          // The virtual keyword is optional here because this
          // method has already been declared virtual in B class
          /* virtual */ const D& operator =(const B& b){
              // Copy fields for base class
              B::operator =(b);
              try{
                  const D& d = dynamic_cast<const D&>(b);
                  // Copy D fields
                  y = d.y;
              }
              catch (std::bad_cast){
                  // Set default values or do nothing
              }
              return *this;
          }
      
          // Overload the assignment operator
          // It is required to have the virtual keyword because
          // you are defining a new method. Even if other methods
          // with the same name are declared virtual it doesn't
          // make this one virtual.
          virtual const D& operator =(const D& d){
              // Copy fields from B
              B::operator =(d);
              // Copy D fields
              y = d.y;
              return *this;
          }
      
          int y;
      };
      

      这似乎是一个完整的解决方案,但事实并非如此。这不是一个完整的解决方案,因为当您从 D 派生时,您将需要 1 个运算符 = 采用 const B&、1 个运算符 = 采用 const D& 和一个运算符采用 常量 D2&。结论很明显,operator =() 重载的个数等于超类的个数+1。

      考虑到D2继承了D,我们来看看两个继承的operator =()方法长什么样。

      class D2 : public D{
          /* virtual */ const D2& operator =(const B& b){
              D::operator =(b); // Maybe it's a D instance referenced by a B reference.
              try{
                  const D2& d2 = dynamic_cast<const D2&>(b);
                  // Copy D2 stuff
              }
              catch (std::bad_cast){
                  // Set defaults or do nothing
              }
              return *this;
          }
      
          /* virtual */ const D2& operator =(const D& d){
              D::operator =(d);
              try{
                  const D2& d2 = dynamic_cast<const D2&>(d);
                  // Copy D2 stuff
              }
              catch (std::bad_cast){
                  // Set defaults or do nothing
              }
              return *this;
          }
      };
      

      很明显 operator =(const D2&) 只是复制字段,想象它就在那里。我们可以注意到继承的运算符 =() 重载中的模式。遗憾的是,我们无法定义能够处理这种模式的虚拟模板方法,我们需要多次复制和粘贴相同的代码以获得完整的多态赋值运算符,这是我看到的唯一解决方案。也适用于其他二元运算符。


      编辑

      正如 cmets 中提到的,为了让生活更轻松,至少可以定义最顶层的超类赋值运算符 =(),并从所有其他超类运算符 =() 方法中调用它。复制字段时也可以定义 _copy 方法。

      class B{
      public:
          // _copy() not required for base class
          virtual const B& operator =(const B& b){
              x = b.x;
              return *this;
          }
      
          int x;
      };
      
      // Copy method usage
      class D1 : public B{
      private:
          void _copy(const D1& d1){
              y = d1.y;
          }
      
      public:
          /* virtual */ const D1& operator =(const B& b){
              B::operator =(b);
              try{
                  _copy(dynamic_cast<const D1&>(b));
              }
              catch (std::bad_cast){
                  // Set defaults or do nothing.
              }
              return *this;
          }
      
          virtual const D1& operator =(const D1& d1){
              B::operator =(d1);
              _copy(d1);
              return *this;
          }
      
          int y;
      };
      
      class D2 : public D1{
      private:
          void _copy(const D2& d2){
              z = d2.z;
          }
      
      public:
          // Top-most superclass operator = definition
          /* virtual */ const D2& operator =(const B& b){
              D1::operator =(b);
              try{
                  _copy(dynamic_cast<const D2&>(b));
              }
              catch (std::bad_cast){
                  // Set defaults or do nothing
              }
              return *this;
          }
      
          // Same body for other superclass arguments
          /* virtual */ const D2& operator =(const D1& d1){
              // Conversion to superclass reference
              // should not throw exception.
              // Call base operator() overload.
              return D2::operator =(dynamic_cast<const B&>(d1));
          }
      
          // The current class operator =()
          virtual const D2& operator =(const D2& d2){
              D1::operator =(d2);
              _copy(d2);
              return *this;
          }
      
          int z;
      };
      

      不需要 set defaults 方法,因为它只会接收一次调用(在基本运算符 =() 重载中)。复制字段时的更改在一个地方完成,所有运算符 =() 重载都会受到影响并实现其预期目的。

      感谢sehe 的建议。

      【讨论】:

      • 我认为阻止默认生成的复制构造函数可能是最简单的。 D&amp; operator=(D const&amp;) = delete;。如果您必须让它可复制分配,那么至少将实现中继到基本情况的虚拟方法。很快,这成为 Cloneable 模式的候选者,因此您可以使用 private virtuals as in GotW18 并减少混淆。换句话说,多态类不能很好地与值语义相结合。永远不会。代码表明隐藏很困难。责任完全在于开发人员...
      • 这还不够,因为如果我删除 D 的 operator =(const D&) 我将无法执行 D d1, d2; 之类的操作。 d1 = d2;
      • 呃。这不是我说的吗?我说,那会最简单。超过 60% 的评论文本涉及“if you must have it copy-assignable”... :)
      • 是的,我的错。调用基本运算符 =() 确实简化了事情。
      【解决方案4】:

      虚拟赋值用于以下场景:

      //code snippet
      Class Base;
      Class Child :public Base;
      
      Child obj1 , obj2;
      Base *ptr1 , *ptr2;
      
      ptr1= &obj1;
      ptr2= &obj2 ;
      
      //Virtual Function prototypes:
      Base& operator=(const Base& obj);
      Child& operator=(const Child& obj);
      

      案例1:obj1 = obj2;

      在这个虚拟概念中没有任何作用,因为我们在 Child 类上调用 operator=

      案例 2&3:*ptr1 = obj2;
      *ptr1 = *ptr2;

      这里的分配不会像预期的那样。 operator= 的原因是在 Base 类上调用。

      可以使用以下任一方法进行纠正:
      1) 铸造

      dynamic_cast<Child&>(*ptr1) = obj2;   // *(dynamic_cast<Child*>(ptr1))=obj2;`
      dynamic_cast<Child&>(*ptr1) = dynamic_cast<Child&>(*ptr2)`
      

      2) 虚拟概念

      现在仅仅使用virtual Base&amp; operator=(const Base&amp; obj) 将无济于事,因为ChildBaseoperator= 的签名不同。

      我们需要在 Child 类中添加 Base&amp; operator=(const Base&amp; obj) 以及它通常的 Child&amp; operator=(const Child&amp; obj) 定义。包含后面的定义很重要,因为在没有默认赋值运算符的情况下将被调用。(obj1=obj2 可能不会给出想要的结果)

      Base& operator=(const Base& obj)
      {
          return operator=(dynamic_cast<Child&>(const_cast<Base&>(obj)));
      }
      

      案例4:obj1 = *ptr2;

      在这种情况下,编译器在 Child 中查找 operator=(Base&amp; obj) 定义,因为在 Child 上调用了 operator=。但由于它不存在并且Base 类型不能隐式提升为child,它会通过错误。(需要像obj1=dynamic_cast&lt;Child&amp;&gt;(*ptr1); 一样进行转换)

      如果我们按照case2&3来实现,这个场景就可以处理了。

      可以看出,在使用基类指针/引用进行赋值的情况下,虚拟赋值使调用更加优雅。

      我们可以让其他运营商也虚拟化吗? 是的

      【讨论】:

      • 感谢您的回答。我发现它准确而清晰,这帮助我解决了我朋友的 c++ 作业的问题。 :)
      • 在 (2) 的示例代码中,使用 dynamic_cast&lt;const Child &amp;&gt;(obj) 代替 dynamic_cast&lt;Child&amp;&gt;(const_cast&lt;Base&amp;&gt;(obj)) 不是更有意义吗?
      • 促销适用于内置类型(shortint...)。
      【解决方案5】:

      仅当您要保证从您的类派生的类的所有成员都正确复制时才需要。如果你没有对多态性做任何事情,那么你真的不需要担心这个。

      我不知道有什么会阻止你虚拟化任何你想要的操作符——它们只是特殊情况的方法调用。

      This page 对这一切的工作原理进行了出色而详细的描述。

      【讨论】:

      • 该页面有一些错误。他用作切片示例的代码实际上并未切片。这忽略了分配无论如何都是非法的事实(const / non-const不匹配)。
      猜你喜欢
      • 1970-01-01
      • 2011-04-15
      • 2016-10-01
      • 1970-01-01
      • 2018-06-21
      • 1970-01-01
      • 1970-01-01
      • 2011-07-29
      • 1970-01-01
      相关资源
      最近更新 更多