C ++中的赋值运算符可以是虚拟的。为什么需要它?我们也可以让其他运营商虚拟化吗?
答案 0 :(得分: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;
}
答案 1 :(得分:24)
这取决于操作员。
将赋值运算符设置为虚拟的目的是让您能够覆盖它以复制更多字段。
所以如果你有一个Base&amp;而你实际上有一个派生&amp;作为动态类型,Derived有更多字段,正确的东西被复制。
然而,存在LHS是Derived的风险,并且RHS是Base,因此当虚拟运算符在Derived中运行时,您的参数不是Derived,并且您无法从中获取字段。
这是一个很好的讨论: http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html
答案 2 :(得分:6)
Brian R. Bondy写道:
将所有这些结合在一起的最后一步,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&amp; 参数,该参数不是虚拟的,并且不会按照您的想法执行。
第二个问题是返回类型,您将返回对派生实例的基本引用。可能没什么问题,因为代码无论如何都有效。最好还是相应地返回引用。
第三个问题,派生类型赋值运算符不调用基类赋值运算符(如果有要复制的私有字段会怎样?),将赋值运算符声明为虚拟运算符不会使编译器为您生成一个。这是一个副作用,即没有至少两个赋值运算符的重载来获得想要的结果。
考虑基类(与我引用的帖子中的基类相同):
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&amp; ,1运算符=需要 const D&amp; 和一个运算符采用 const D2&amp; 。结论很明显,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&amp;)只是复制字段,想象就好像它在那里一样。我们可以注意到继承的operator =()重载中的模式。遗憾的是,我们无法定义将处理此模式的虚拟模板方法,我们需要多次复制和粘贴相同的代码才能获得完整的多态分配运算符,这是我看到的唯一解决方案。也适用于其他二元运算符。
正如评论中所提到的,为了简化生活,可以做的最少的事情是定义最顶层的超类赋值operator =(),并从所有其他超类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;
};
不需要 set defaults 方法,因为它只接收一个调用(在base operator =()重载中)。复制字段的更改在一个地方完成,所有operator =()重载都会受到影响并实现其预期目的。
感谢sehe提出建议。
答案 3 :(得分: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&amp; 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)
无效,因为Child
的{{1}}和Base
的签名不同。
我们需要在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
一样进行投射)
如果我们按照case2&amp; 3实施,将会处理这种情况。
可以看出,虚拟分配使得使用基类指针/引用进行赋值时调用更加优雅。
我们可以让其他运营商也虚拟化吗?的是
答案 4 :(得分:4)
只有当您想要保证从您的类派生的类才能正确复制其所有成员时才需要它。如果你没有对多态性做任何事情,那么你真的不需要担心这个。
我不知道任何阻止你虚拟化你想要的任何操作符的东西 - 它们只是特殊情况方法调用。
This page提供了一个非常详细的描述,说明了这一切是如何运作的。
答案 5 :(得分:3)
运算符是一种具有特殊语法的方法。你可以像对待任何其他方法一样对待它......