虚拟赋值运算符 C++

78

C++中的赋值操作符可以被定义为虚函数。为什么需要这样做?其他运算符能否也变成虚函数?

5个回答

65
赋值运算符不需要被声明为虚函数。
下面讨论的是关于operator=的,但它也适用于任何接受相应类型参数的重载操作符以及任何接受相应类型参数的函数。
下面的讨论显示了虚关键字在查找匹配函数签名时并不知道参数的继承。在最后一个示例中,展示了如何正确处理涉及继承类型的赋值操作。
虚函数无法识别参数的继承关系:
要想使用虚函数,函数的签名必须相同。因此,即使在下面的示例中,operator= 被声明为虚函数,由于 operator= 的参数和返回值不同,在 D 中,调用永远不会作为虚函数。
函数 B::operator=(const B& right)D::operator=(const D& right) 是完全不同的两个函数,它们被视为两个不同的函数。
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;
};

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

你可以定义一个虚函数来设置在变量类型为B的情况下D的默认值,即使你的B变量实际上是一个存储在B引用中的D。在这种情况下,你将不会得到 D::operator=(const D& right) 函数。

在以下情况下,当两个D对象存储在两个B引用中时,将使用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的值不会被初始化。也就是说,它将使用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,我在这个问题中发现了一些奇怪的行为:https://dev59.com/qXNA5IYBdhLWcg3wa9Kp。你有什么想法吗? - David Rodríguez - dribeas
我理解你关于虚拟使用的论点,但在你的最终代码中,你使用了“const D *pD = dynamic_cast<const D*>(&right);”,这似乎不正确放置在基类中。你能解释一下吗? - Jake88
3
这并不在基类里。它在派生类中重写了虚拟operator=运算符,这个运算符最初是在基类中声明的。 - Ben Voigt
消除这个问题最简单的方法是将派生类的复制赋值运算符标记为“override”,然后代码就不会编译,这证明了你关于两个运算符(基类和派生类的=运算符)不同的猜测是正确的:class Derived : public Base{Derived& operator=(const Derived&) override {return *this;}};现在Derived' = 运算符会让编译器在其基类中搜索相应的成员,当然这会失败并生成一个错误。 - Maestro
虽然我们可以使用=多态,但这没有意义,因为派生类版本必须具有相同的签名,这意味着它应该采用对基类的引用而不是派生类:struct D : B{D& operator=(const B&)override{return *this;}}; 虽然它编译通过,但需要将该引用从基类转换为派生类。 - Maestro

26

这取决于运算符。

使赋值运算符虚拟的要点是允许您获得覆盖它以复制更多字段的好处。

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

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

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


9

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;
}
我想对这个解决方案进行一些补充说明。将赋值运算符声明为与上述相同会出现三个问题。
第一个问题是编译器生成的赋值运算符使用了一个非虚拟的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个operator=,它需要const B&,1个operator=,它需要const D&和一个operator,它需要const 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&) 只是复制字段,就好像它一直在那里。我们可以在继承的 operator =() 重载中注意到一个模式。不幸的是,我们不能定义虚拟模板方法来处理这个模式,我们需要多次复制和粘贴相同的代码,以获得完整的多态赋值运算符,这是我所看到的唯一解决方案。也适用于其他二元操作符。

编辑

如评论中所述,为了使生活更轻松,最少可做的是定义最顶层超类的赋值运算符=(),并从所有其他超类的赋值运算符=()方法中调用它。此外,在复制字段时,可以定义一个名为 _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;
};

不需要一个设置默认值的方法,因为它只会接收一次调用(在基本的operator =()重载中)。复制字段时的更改在一个地方完成,所有operator =()重载都受到影响并承载其预期目的。

感谢sehe的建议。


我认为防止默认生成的复制构造函数可能是最简单的。D& operator=(D const&) = delete;。如果你必须使它可复制赋值,那么至少将实现中继到基本情况的虚拟方法。很快这就成为了Cloneable模式的候选,所以你可以使用私有虚拟函数,如GotW18,同时也更不容易混淆。换句话说,多态类与值语义不相容。永远不会。代码表明隐藏是困难的。完全取决于开发人员... - sehe
这是不够的,因为如果我删除D的操作符=(const D&),我将无法执行以下操作: D d1, d2; d1 = d2; - Andrei15193
额,这不是我说的吗?我说过,这是最简单的方法。超过60%的评论文本都涉及“_如果你必须要它可以复制赋值_”的情况... :) - sehe
是的,我的错。调用基本运算符=()确实简化了事情。 - Andrei15193

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;

在这种情况下,虚拟概念不起任何作用,因为我们在Child类上调用了operator=

情况2和3:*ptr1 = obj2;
                  *ptr1 = *ptr2;

这里的赋值不会按预期进行。原因是调用了Base类上的operator=

可以通过以下方式纠正:
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)是不够的,因为ChildBaseoperator=签名不同。

我们需要在Child类中添加Base& operator=(const Base& obj)以及它通常的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)的定义,因为operator=是在Child上调用的。但由于其不存在且Base类型不能隐式转换为child,所以会抛出错误。(需要像obj1=dynamic_cast<Child&>(*ptr1);一样进行强制转换)

如果按照情况2&3实现,则可以处理此场景。

正如可以看到的那样,在使用Base类指针/引用进行赋值时,虚拟赋值使调用更加优雅。

我们还可以将其他运算符也设置为虚拟的吗?可以


1
谢谢你的回答。我发现它非常精确和清晰,这帮助我解决了我朋友的C++作业问题。 :) - Jake88
在你的示例代码中(2),使用dynamic_cast<const Child &>(obj)而不是dynamic_cast<Child&>(const_cast<Base&>(obj))会更有意义,不是吗? - Nemo
促销是针对内置类型(从shortint...)的。 - curiousguy

4

只有在您想要保证从您的类派生的类正确复制其所有成员时才需要使用。如果您没有使用多态性,那么您不需要担心这个问题。

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

此页面提供了关于所有这些工作原理的优秀和详细的描述。


1
那个页面上有几个错误。他用作切片示例的代码实际上并没有进行切片。而且这还忽略了分配不合法的事实(const/non-const 不匹配)。 - Functastic

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