虚拟赋值运算符C ++


Answers:


53

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

下面的讨论是关于的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)从未使用过。

如果不使用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;
}

Brian,我发现此问题代表了一些奇怪的行为:stackoverflow.com/questions/969232/…。你有什么想法?
DavidRodríguez-dribeas,2009年

我理解您对虚拟用法的争论,但是在最后一篇文章中,您使用了'const D * pD = dynamic_cast <const D *>(&right);',这似乎不适用于基类。你可以解释吗?
Jake88

3
@ Jake88:那不在基类中。它是派生类中对虚拟运算符=的重写,该运算符首先在基类中声明。
Ben Voigt

解决问题的最简单方法是将派生类的副本分配运算符标记为“ override”,然后代码将无法编译,这证明您对2个运算符的猜测是不同的(= from base和named):class Derived: public Base {Derived&operator =(const Derived&)override {return * this;}}; 现在,Derived'=运算符会导致编译器在其基础中搜索相应的成员,当然它会失败并生成错误。
Maestro

尽管我们可以使用=多态,但这没有意义,因为派生类的版本必须具有相同的签名,这意味着它应该引用不派生的基数:struct D:B {D&operator =(const B&)override {返回* this;}}; 尽管可以编译,但需要将该引用转换为base到衍生物。
艺术大师

25

这取决于操作员。

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

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

但是,存在这样的风险:您的LHS是派生的,而RHS是基本的,因此当虚拟运算符在派生中运行时,您的参数不是派生的,并且您将无法从中获取字段。

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


7

布莱恩·邦迪写道:


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&参数,该参数不是虚拟的,并且没有执行您可能认为的操作。

第二个问题是返回类型,您正在返回对派生实例的基本引用。无论如何,代码可能不会有太大的问题。仍然最好相应地返回引用。

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

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

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&,以及一个需要const D2&的运营商。结论是显而易见的,运算符=()重载的数量等于超类的数量+ 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;
    }
};

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


编辑

如评论中所述,使生活变得更轻松的最少方法是定义最顶层的超类赋值运算符=(),并从所有其他超类运算符=()方法中调用它。同样,在复制字段时,可以定义_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 =()重载都将受到影响并具有其预期的目的。

感谢sehe的建议。


我认为防止默认生成的副本构造函数可能最简单。D& operator=(D const&) = delete;。如果必须使它具有可复制分配的功能,则对于基本情况至少应将实现中继到虚拟方法。很快,它就成为了Cloneable模式的候选者,因此您可以像GotW18一样使用私有虚拟机,并且不会造成混乱。换句话说,多态类与值语义不能很好地融合在一起。永远不会。该代码表明隐藏很难。举证责任完全在开发商...
sehe

这还不够,因为如果删除D的运算符=(const D&),我将无法执行D d1,d2之类的操作;d1 = d2;
2012年

恩 那不是我说的吗 我说过,这将是最简单的。注释文本超过60%的处理情况“如果你必须有它复制分配” ... :)
sehe

是的,我不好。调用基本操作者=()确实可以简化事情。
2012年

5

在以下情况下使用虚拟分配:

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

在这种情况下,编译器会operator=(Base& obj)定义Childoperator=叫上孩子。但由于它不存在,且Base类型不能被晋升为child含蓄,它会通过错误。(铸件需要像obj1=dynamic_cast<Child&>(*ptr1);

如果我们根据case2&3实施,则将解决此情况。

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

我们也可以使其他运营商虚拟吗?


1
感谢您的回答。我发现它准确而清晰,这帮助我解决了我朋友的c ++分配问题。:)
Jake88

在(2)的示例代码中,使用dynamic_cast<const Child &>(obj)代替有意义dynamic_cast<Child&>(const_cast<Base&>(obj))吗?
Nemo

促销适用于内置类型(shortint...)。
curiousguy 2015年

4

仅当您要保证从您的类派生的类可以正确复制其所有成员时才需要使用此属性。如果您不对多态性做任何事情,那么您就不必为此担心。

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

此页面提供了一个很好的详细说明。


1
该页面上有一些错误。他用作切片示例的代码实际上并未切片。而且这忽略了这样的事实,即无论如何分配都是非法的(常量/非常量不匹配)。
09年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.