为什么将一个对象赋值给其基类是有效的,但将一个对象赋值给其派生类会出现编译错误?

38

这是一个面试题。考虑以下内容:

struct A {}; 
struct B : A {}; 
A a; 
B b; 
a = b;
b = a; 
为什么执行 b = a; 会抛出错误,而执行 a = b; 则完全没有问题?

3
这其实是一个非常好的问题。第一个陈述句为什么有效并没有明显的原因。 - Konrad Rudolph
我假设结构体的行为就像类一样 - 它只是削减了所有独特的'B'结构项并复制了所有'a'。 - DanTheMan
3
“struct”是一种类,只是默认情况下其成员是公共的。除此之外,它们是相同的。 - R. Martinho Fernandes
我可能会编写一个问题,使用类来深入理解继承的本质。 - JohnMcG
2
@JohnMcG:在C++中,struct就是class。这里没有任何“本质”被混淆。 - Billy ONeal
推荐阅读关于名称隐藏的内容:Effective C++: Tip 33。 - Lazer
6个回答

63

因为 B 的隐式声明拷贝赋值运算符覆盖了 A 的隐式声明拷贝赋值运算符。

所以对于语句 b = a,只有 Boperator= 能够被选中作为候选。但它的参数类型是 B const&,不能通过一个 A 类型的参数进行初始化(需要向下转型)。因此会出现错误。


15
您可以尝试将B的定义更改为struct B : A { using A::operator=; };,观察两行代码都会编译通过。 - Johannes Schaub - litb
好的回答,让人完全明白。其他的回答更多是哲学性的,而这个回答包含了具体的技术原因,说明为什么它不起作用。 - Martin Vilcans
B的隐式声明复制赋值运算符如何隐藏A的隐式声明复制赋值运算符? - maxpayne
@maxpayne 如果在 B 中添加 using A::operator=; 将会修复它,如果这有助于澄清。 - Michael Mrozek
1
@logic_max: `结构体 A {}; 结构体 B { A& operator= (const &A) {return *this} // 它被隐藏了 };A a; B b;` a = b; b = a; // 这里没有编译错误 - badawi
显示剩余3条评论

25

因为每个B都是A,但不是每个A都是B。

根据评论进行了编辑以使事情更加清晰(我修改了你的例子):

struct A {int someInt;}; 
struct B : A {int anotherInt}; 
A a; 
B b; 

/* Compiler thinks: B inherits from A, so I'm going to create
   a new A from b, stripping B-specific fields. Then, I assign it to a.
   Let's do this!
 */
a = b;

/* Compiler thinks: I'm missing some information here! If I create a new B
   from a, what do I put in b.anotherInt?
   Let's not do this!
 */
b = a;
在你的例子中,没有 someIntanotherInt 这些属性,所以它可能会工作。但是编译器无论如何都不会允许它。

8
从哲学角度来看,你的回答是正确的,但只有在使用指针引用对象时,这种哲学才能直接转化为代码。关于隐式的 A::operator=B::operator= 的一些技术细节缺失,其他回答更好地涵盖了这些内容。 - Ken Bloom
1
无论怎样解释,都要给愚蠢的负评点赞+1。 - Cheers and hth. - Alf
@LokiAstari:你错了。答案是正确的,只是不太符合C++技术规范。其他语言也会以稍微不同的方式执行相同的操作。理解“为什么”而不是C++技术细节更好,比记住细节但不理解“为什么”要好。litb的答案在“为什么”方面有些欠缺。 - Cheers and hth. - Alf
@Cheersandhth.-Alf:现在没问题了。没有注释的话就毫无意义了。看看历史记录。 - Martin York

6

确实,B 是一个 A,但是一个 A 不是一个 B,然而这个事实只在你处理指向 AB 的指针或引用时才直接适用。问题在于你的赋值运算符。

struct A {}; 
struct B : A {};

等同于

struct A {
   A& operator=(const A&);
}; 
struct B : A {
   B& operator=(const B&);
};

所以当您分配如下内容时:
A a; 
B b; 
a = b;
a的赋值运算符可以接受b作为参数,因为BA的一种,所以b可以被传递给赋值运算符作为一个A&。需要注意的是,a的赋值运算符只知道A中的数据,而不知道B中的数据,因此B中不属于A的任何成员都会丢失——这就是所谓的“切片”。

但是当你尝试进行赋值时:

b = a; 
a的类型是A,不是B,所以a无法匹配b的赋值运算符的B&参数。你可能认为b=a只会调用继承的A& A::operator=(const A&),但事实并非如此。赋值运算符B& B::operator=(const B&)会隐藏从A继承的运算符。可以使用using A::operator=;声明来恢复其作用。

4
我已经更改了你的结构体名称,以使原因显而易见:
struct Animal {}; 
struct Bear : Animal {}; 
Animal a; 
Bear b; 
a = b; // line 1 
b = a; // line 2 

显然,任何熊都是动物,但并非所有动物都可以被视为熊。

因为每个B“是一个”A,所以B的每个实例也必须是A的实例:根据定义,它具有与A的任何其他实例相同的成员和相同的顺序。将b复制到a中会丢失B特定的成员,但完全填充a的成员,从而得到一个满足A要求的结构体。另一方面,将a复制到b可能会使b不完整,因为B可能比A具有更多的成员。这很难在这里看到,因为A和B都没有任何成员,但这就是编译器允许一种赋值而不允许另一种赋值的原因。


2
这个错误的原因和Cicada的答案一样,详细解释请参见那里的评论。哦,还有其他原因也是错误的(LSP)。 - Konrad Rudolph
2
更改名称对我来说并没有使它更加明显。 - R. Martinho Fernandes
@Martin,你说得很好。Bear怎么样?;-) - Caleb
嗯...鉴于 Animal::setCovering(Covering c);,你可以这样做 Bear b; b.setCovering(Feathers);。因此,你也可以认为 Animal 是 Bear,或者两者都不是彼此的子类。重要的不是谁继承自谁,而是 什么 被继承了。 - R. Martinho Fernandes
如果有人不理解我所指的问题,请参考:https://secure.wikimedia.org/wikipedia/en/wiki/Circle-ellipse_problem。因此,在关于继承的示例中使用矩形/正方形并不是一个好主意,除非你想讨论那个确切的问题。 - Martin Vilcans
显示剩余2条评论

3

请记住,如果没有显式声明复制赋值运算符,那么将为任何类(结构体在C++中也属于类)隐式地声明和定义一个复制赋值运算符。

对于struct A,它的签名如下:

A& A::operator=(const A&)

它只是对其子对象执行逐成员赋值。 a = b; 是可以的,因为 B 会与 A::operator=(const A&)const A& 参数匹配。由于仅将 A 的成员“逐成员赋值”到目标,因此不属于 A 的任何 B 成员都会丢失 - 这被称为“切片”。
对于 struct B,隐式赋值运算符将具有以下签名:
B& B::operator=(const B&)

b = a;不可以,因为Aconst B&参数不匹配。


1

如果我正在接受面试,那么我会用一些哲学的方式来解释。

a = b;

是有效的,因为每个B都包含A作为其一部分。 因此,a可以从B内提取A。 但是,A不包含B。 因此,b无法从A内找到B;这就是为什么,

b = a;

无效。

[类比地说,void* 可以在任何 Type* 中找到,但 Type* 不能在 void* 中找到(因此我们需要一个强制转换)。]


这也是错误的。第一次转换是有损的(即它会导致信息丢失)。那么为什么它是有效的?它不应该是有效的。你的类比是有缺陷的,因为转换为void*不是有损的。 - Konrad Rudolph
4
@Konrad,我不同意。信息确实会丢失,但是b对象中的B特定部分并不需要让对象成为有效的A,因此可以切掉B特定成员。这 确实 是C ++标准中有争议的部分,因为它会对非虚拟函数造成问题。对于纯数据对象,不应该有问题。 - Martin Vilcans
1
即使是普通的数据对象也会受到这种损失的影响;考虑从 doubleint 的(允许的)隐式转换。C++ 允许这样做的事实是荒谬的。大多数现代语言都禁止这样做,因为它很危险。 - Konrad Rudolph
@Konrad,当我们执行a = b;时,损失是可以预料的。这就是为什么它被称为切片。类比只是为了支持自然转换。 - iammilind
2
@Konrad,请再次阅读问题。它特别涉及到结构体的子类化。int和double都不是彼此的子类。 - Martin Vilcans
显示剩余5条评论

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