虚拟赋值运算符C ++

问题描述 投票:64回答:6

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

c++ operator-overloading virtual virtual-functions
6个回答
47
投票

赋值运算符不需要是虚拟的。

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

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


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

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

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

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)上的虚拟关键字,您将获得与上述相同的结果,但y的值不会被初始化。即它会使用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;
}

24
投票

这取决于运营商。

使赋值运算符虚拟化的目的是让您能够覆盖它以复制更多字段。

因此,如果您有一个Base&并且您实际上有一个Derived&作为动态类型,并且Derived有更多字段,则会复制正确的内容。

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

这是一个很好的讨论:qazxsw poi


6
投票

http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html写道:


将所有这些结合在一起的最后一步,RTTI:

您可以使用RTTI来正确处理您的类型中的虚拟函数。这是解决在处理可能继承的类型时如何正确处理赋值的最后一部分。

Brian R. Bondy

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

编译器生成一个赋值运算符,它接受一个非虚拟的const D&参数,并且不会执行您认为它做的事情。

第二个问题是返回类型,您将返回对派生实例的基本引用。可能没什么问题,因为代码无论如何都有效。仍然最好相应地返回引用。

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

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

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;
}

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

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

    int x;
};

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

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

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;
};

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


编辑

正如评论中所提到的,为了简化生活,可以做的最少的事情是定义最顶层的超类赋值operator =(),并从所有其他超类operator =()方法中调用它。此外,在复制字段时,可以定义_copy方法。

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;
    }
};

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

感谢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; }; 的建议。


5
投票

虚拟分配用于以下场景:

sehe

案例1:obj1 = obj2;

在这个虚拟概念中,我们在//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); 类上调用operator=并没有发挥任何作用。

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

这里的任务不会像预期的那样。 Child的原因是在operator=类上调用。

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

Base

2)虚拟概念

现在简单地使用dynamic_cast<Child&>(*ptr1) = obj2; // *(dynamic_cast<Child*>(ptr1))=obj2;` dynamic_cast<Child&>(*ptr1) = dynamic_cast<Child&>(*ptr2)` 将无济于事,因为virtual Base& operator=(const Base& obj)ChildBase签名不同。

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

obj1=obj2

案例4:obj1 = * ptr2;

在这种情况下,编译器在Base& operator=(const Base& obj) { return operator=(dynamic_cast<Child&>(const_cast<Base&>(obj))); } 中查找operator=(Base& obj)定义,因为在Child上调用Child。但由于它不存在和operator=类型不能隐含地提升为Base,它将通过错误。(像child一样需要强制转换)

如果我们按照案例2和3实施,这个场景将被处理。

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

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


4
投票

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

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

obj1=dynamic_cast<Child&>(*ptr1);提供了一个非常详细的描述,说明了这一切是如何工作的。


3
投票

运算符是一种具有特殊语法的方法。你可以像对待任何其他方法一样对待它......

© www.soinside.com 2019 - 2024. All rights reserved.