为什么虚拟赋值运算符与其他具有相同参数的虚拟函数表现不同?

22

在实现虚拟赋值运算符时,我遇到了一个有趣的问题。这不是编译器故障,因为g++ 4.1、4.3和VS 2005具有相同的行为。

基本上,虚拟operator=与任何其他虚拟函数在实际执行的代码方面有所不同。

struct Base {
   virtual Base& f( Base const & ) {
      std::cout << "Base::f(Base const &)" << std::endl;
      return *this;
   }
   virtual Base& operator=( Base const & ) {
      std::cout << "Base::operator=(Base const &)" << std::endl;
      return *this;
   }
};
struct Derived : public Base {
   virtual Base& f( Base const & ) {
      std::cout << "Derived::f(Base const &)" << std::endl;
      return *this;
   }
   virtual Base& operator=( Base const & ) {
      std::cout << "Derived::operator=( Base const & )" << std::endl;
      return *this;
   }
};
int main() {
   Derived a, b;

   a.f( b ); // [0] outputs: Derived::f(Base const &) (expected result)
   a = b;    // [1] outputs: Base::operator=(Base const &)

   Base & ba = a;
   Base & bb = b;
   ba = bb;  // [2] outputs: Derived::operator=(Base const &)

   Derived & da = a;
   Derived & db = b;
   da = db;  // [3] outputs: Base::operator=(Base const &)

   ba = da;  // [4] outputs: Derived::operator=(Base const &)
   da = ba;  // [5] outputs: Derived::operator=(Base const &)
}
虚拟赋值运算符与具有相同签名的任何其他虚拟函数相比,其效果是不同的([0]与[1]进行比较),当通过真实的派生对象([1])或派生引用([3])调用时,它将调用运算符的基本版本,而当通过基本引用([2])调用或左值或右值是基本引用而另一个是派生引用时([4],[5]),它会像普通虚拟函数一样执行。这种奇怪的行为有没有合理的解释?
5个回答

14

以下是步骤:

如果我将[1]更改为

a = *((Base*)&b);

那么事情会按照你的期望进行。在 Derived 中会有一个自动生成的赋值运算符,看起来像这样:

Derived& operator=(Derived const & that) {
    Base::operator=(that);
    // rewrite all Derived members by using their assignment operator, for example
    foo = that.foo;
    bar = that.bar;
    return *this;
}
在你的示例中,编译器有足够的信息来猜测ab的类型是Derived,因此它们选择使用自动生成的调用你的函数的操作符。这就是你得到 [1] 的原因。我的指针转换强制编译器按照你的方式进行,因为我告诉编译器“忘记”bDerived类型,所以它使用Base。其他结果也可以用同样的方式解释。

3
这里没有猜测的余地。规则非常严格。 - MSalters
谢谢,真正的答案(已由三个人发布)是,派生类的编译器生成operator=会隐式调用Base::operator=。我将其标记为“接受的答案”,因为它是第一个回答的。 - David Rodríguez - dribeas
a = static_cast<Base &>(b); 是避免使用 C 风格转换(可能会意外执行重新解释转换)的一种方法。 - M.M

5

在这种情况下,有三个operator=:

Base::operator=(Base const&) // virtual
Derived::operator=(Base const&) // virtual
Derived::operator=(Derived const&) // Compiler generated, calls Base::operator=(Base const&) directly

这就解释了为什么在情况[1]中看起来像是虚拟调用了Base :: operator =(Base const&)。它是从编译器生成的版本调用的。情况[3]也适用于此。在情况2中,右侧参数“bb”的类型为Base&,因此无法调用Derived :: operator =(Derived&)。


4

Derived类没有定义用户提供的赋值运算符。因此,编译器会合成一个,并在Derived类的合成赋值运算符中从内部调用基类赋值运算符。

virtual Base& operator=( Base const & ) //is not assignment operator for Derived

因此,a = b; // [1] 输出:Base::operator=(Base const &) 在派生类中,基类的赋值运算符已被重写,因此,重写的方法会在派生类的虚表中获得一个条目。当通过引用或指针调用该方法时,在运行时由于虚表条目解析,派生类的重写方法会被调用。
ba = bb;  // [2] outputs: Derived::operator=(Base const &)

==>内部实现==>(对象->VTable[赋值运算符]) 获取对象所属类别的VTable中赋值运算符的条目,并调用该方法。


3
如果您未能提供适当的operator=(即正确的返回类型和参数类型),则编译器会提供默认的operator=,该操作符会重载任何用户定义的操作符。在您的情况下,它将在复制Derived成员之前调用Base::operator=(Base const&)
有关将operator=设置为虚拟的详细信息,请查看此链接

2
由于编译器提供了默认的分配运算符operator=,所以在a = b的情况下会调用它,并且我们知道默认内部调用基本的分配运算符。
更多关于虚拟分配的解释可以在此找到:https://dev59.com/DnRB5IYBdhLWcg3wSVi1#26906275

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接