将Derived**转换为Base**,将Derived*转换为Base*

4

好的,我正在阅读这篇FQA中的条目,涉及将Derived**转换为Base**的问题以及为什么它是被禁止的。我明白问题在于你可能会将一个不是Derived*的东西赋值给Base*,因此我们禁止了这种情况。

到目前为止,一切顺利。

但是,如果我们深入应用这个原则,为什么我们不禁止这样的例子呢?

void nasty_function(Base *b)
{
  *b = Base(3); // Ouch!
}

int main(int argc, char **argv)
{
  Derived *d = new Derived;
  nasty_function(d); // Ooops, now *d points to a Base. What would happen now?
}

我认为 nasty_function 做了一些愚蠢的事情,所以我们可以说允许这种类型的转换是可以的,因为我们可以实现有趣的设计,但我们也可以说对于双重间接性也是如此:你得到一个 Base **,但你不应该给它的延迟赋值,因为你真的不知道那个 Base ** 来自哪里,就像 Base * 一样。

所以问题是:那个额外的间接级别有什么特别之处?也许关键在于,只有一个间接级别时,我们可以使用虚拟 operator= 进行操作,以避免这种情况,而在普通指针上没有相同的机制可用?


嗯,那个FQA/FAQ难道不是恰好回答了你的问题吗? - J-16 SDiZ
并不是真的,因为它们没有指出您可以使用普通指针执行相同的技巧。 - akappa
6个回答

16
nasty_function(d); // Ooops, now *d points to a Base. What would happen now?

不是的,它指向一个“Derived”。该函数只是改变了现有“Derived”对象中的“Base”子对象。请考虑:
#include <cassert>

struct Base {
    Base(int x) : x(x) {}
    int x;
};
struct Derived : Base {
     Derived(int x, int y) : Base(x), y(y) {}
     int y;
};

int main(int argc, char **argv)
{
  Derived d(1,2); // seriously, WTF is it with people and new?
                  // You don't need new to use pointers
                  // Stop it already
  assert(d.x == 1);
  assert(d.y == 2);
  nasty_function(&d);
  assert(d.x == 3);
  assert(d.y == 2);
}

d不会神奇地变成一个Base,对吧?它仍然是一个Derived,但是它的Base部分发生了改变。


图片中展示了 BaseDerived 对象的样子:

Layouts

当我们有两个间接级别时,它无法工作,因为被分配的东西是指针:

Assigning pointers - type mismatch

请注意,上述问题中既没有尝试更改Base对象,也没有尝试更改Derived对象,只有中间指针被更改了。
但是,当您只有一级间接引用时,代码会以对象允许的方式修改对象本身(可以通过使Base中的赋值运算符成为私有、隐藏或删除来禁止它)。

Assigning with only one level of indirection

请注意这里没有改变任何指针。这就像改变对象的一部分的任何其他操作一样,比如 d.y = 42;

1
是的,我认为我应该在我的心理模型中保留那个“子对象”的概念。顺便说一下,你在“new and pointer”中的评论过于苛刻了,因为它是用来展示一个概念的代码。 - akappa
不,我认为这是正确的哈希量。有太多的Java程序员在C++的每一行上写“new”,这太傻了。 - Mooing Duck
@akappa 采用现代C++语言风格的概念验证编写时间更短,而且仍然是正确的,不像采用丑陋的、早于98年的思想的概念验证。 - Etienne de Martel
但我这样做是因为我们正在讨论指针转换,所以使用Derived *变量可以使其更加明确。我本来可以这样写:Derived a; Derived *b = &a,但我认为仅仅因为“在这里使用new是无用的,应该像地狱一样避免”,就这样做有些荒谬——我的意思是,即使我们编程,也应该保留一些灵活性。 - akappa

7
不,nasty_function()并不像它听起来那么难以处理。由于指针b指向的是一个is-aBase的东西,将一个Base值分配给它是完全合法的。
请注意:你的“哎呀”注释是不正确的:d仍然指向调用之前相同的Derived!只是它的Base部分被重新赋值了(按值!)。如果这让整个Derived失去一致性,那么你需要通过使Base::operator=()成为虚函数来重新设计。然后,在nasty_function()中,事实上会调用Derived赋值运算符(如果已定义)。
因此,我认为你的示例与指针到指针的情况没有太大关系。

2
"

*b = Base(3) 调用了 Base::operator=(const Base&),由于成员函数(包括运算符)被继承,实际上该函数也存在于 Derived 中。

如果接下来调用 Derived::operator=(const Base&),有时会被称为"切割",并且通常是不好的。这是 C++ 中“变得像”运算符(=)无处不在的悲哀后果。

(请注意,“变得像”运算符在大多数面向对象语言(如 Java、C# 或 Python)中不存在;在对象上下文中,= 的含义是引用赋值,类似于 C++ 中的指针赋值。)

"
总结:
Derived** 转换为 Base** 是被禁止的,因为它们可能会导致类型错误,因为这样你可能会得到一个指向 Base 类型对象的 Derived* 类型指针。
你提到的问题不是类型错误,而是一种不同类型的错误:对 derived 对象接口的误用,这源于它继承了其父类的“变成”运算符。
(是的,我故意将对象上下文中的op =称为“变成类似”,因为我觉得“赋值”并不是一个好的名称来说明这里发生了什么。)

是的,表面上看起来它们是相同的问题,但它们实际上是两个完全不同的东西。谢谢! - akappa

0

好的,你提供的代码是有意义的。确实,赋值运算符不能覆盖Derived特定的数据,只能覆盖Base的数据。虚函数仍然来自Derived而不是Base。


0
*b = Base(3); // Ouch!

在这里,*b 对象确实是一个 B,它是 *d 的基类子对象。只有该基类子对象被修改,派生对象的其余部分不会改变,d 仍然指向派生类型的同一对象。

您可能不希望允许修改基类,但从类型系统的角度来看,这是正确的。一个 Derived 是一个 Base

对于非法指针情况,这并不正确。一个 Derived* 可以转换为 Base*,但它们不是相同的类型。这违反了类型系统。

允许您所询问的转换与以下情况没有区别:

Derived* d;
Base b;
d = &b;
d->x;

0

阅读了我的问题的好答案,我认为我理解了问题的关键所在,这与子对象和运算符重载无关,而是来自面向对象编程的基本原则。

关键在于你可以在需要一个Base的地方使用Derived(替换原则),但是由于可能将派生类实例的指针分配给它,因此你不能在需要Base*的地方使用Derived*

考虑一个具有此原型的函数:

void f(Base **b)

f 可以对 b 进行许多操作,包括取消引用等:

void f(Base **b)
{
  Base *pb = *b;
  ...
}

如果我们将f传递给Derived**,这意味着我们正在使用Derived*作为Base*,这是不正确的,因为我们可以将OtherDerived*分配给Base*但不能分配给Derived*
另一方面,考虑这个函数:
void f(Base *b)

如果 f 解引用 b,那么我们将使用一个 Derived 替代 Base,这完全没问题(前提是您正确实现了类层次结构):
void f(Base *b)
{
  Base pb = *b; // *b is a Derived? No problem!
}

换句话说,替换原则(使用派生类代替基类)适用于实例而不是指针,因为指向A的指针的“概念”是“指向继承A的任何类的实例”,而继承Base的类的集合严格包含继承Derived的类的集合。

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