74

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

4

5 回答 5

58

不需要将赋值运算符设为虚拟。

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

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


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

函数的签名必须相同才能使 virtual 发挥作用。因此,即使在下面的示例中,operator= 被设置为 virtual,调用也不会在 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 类型的变量时为其设置默认值。即使您的 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)从未使用过。

如果没有 virtual 关键字,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;
}
于 2009-03-21T20:05:59.430 回答
25

这取决于运营商。

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

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

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

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

于 2009-03-21T19:34:45.653 回答
8

Brian R. Bondy wrote:


One last step to tie it all together, RTTI:

You can use RTTI to properly handle virtual functions that take in your type. Here is the last piece of the puzzle to figure out how to properly handle assignment when dealing with possibly inherited types.

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

I would like to add to this solution a few remarks. Having the assignment operator declared the same as above has three issues.

The compiler generates an assignment operator that takes a const D& argument which is not virtual and does not do what you may think it does.

Second issue is the return type, you are returning a base reference to a derived instance. Probably not much of an issue as the code works anyway. Still it is better to return references accordingly.

Third issue, derived type assignment operator does not call base class assignment operator (what if there are private fields that you would like to copy?), declaring the assignment operator as virtual will not make the compiler generate one for you. This is rather a side effect of not having at least two overloads of the assignment operator to get the wanted result.

Considering the base class (same as the one from the post I quoted):

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

    int x;
};

The following code completes the RTTI solution that I quoted:

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

This may seem a complete solution, it's not. This is not a complete solution because when you derive from D you will need 1 operator = that takes const B&, 1 operator = that takes const D& and one operator that takes const D2&. The conclusion is obvious, the number of operator =() overloads is equivalent with the number of super classes + 1.

Considering that D2 inherits D, let's take a look at how the two inherited operator =() methods look like.

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

It is obvious that the operator =(const D2&) just copies fields, imagine as if it was there. We can notice a pattern in the inherited operator =() overloads. Sadly we cannot define virtual template methods that will take care of this pattern, we need to copy and paste multiple times the same code in order to get a full polymorphic assignment operator, the only solution I see. Also applies to other binary operators.


Edit

As mentioned in the comments, the least that can be done to make life easier is to define the top-most superclass assignment operator =(), and call it from all other superclass operator =() methods. Also when copying fields a _copy method can be defined.

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

There is no need for a set defaults method because it would receive only one call (in the base operator =() overload). Changes when copying fields are done in one place and all operator =() overloads are affected and carry their intended purpose.

Thanks sehe for the suggestion.

于 2012-09-04T14:07:30.490 回答
6

虚拟分配用于以下场景:

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

operator=在这个虚拟概念中,我们在Child课堂上调用的任何角色都没有。

案例 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& operator=(const Base& obj)将无济于事,因为签名在ChildBasefor中是不同的operator=

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

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

案例 4:obj1 = *ptr2;

在这种情况下,编译器在 Child上调用时查找operator=(Base& obj)定义。但是由于它不存在并且类型不能被隐式提升,它将通过错误。(需要强制转换,如)Childoperator=Basechildobj1=dynamic_cast<Child&>(*ptr1);

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

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

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

于 2014-11-13T10:11:02.327 回答
4

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

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

此页面对所有这些工作原理进行了出色而详细的描述。

于 2009-03-21T19:31:17.970 回答