【问题标题】:Converting Derived** to Base** and Derived* to Base*将 Derived** 转换为 Base** 并将 Derived* 转换为 Base*
【发布时间】:2012-06-29 15:26:22
【问题描述】:

好的,我正在阅读 this entry in the FQA 处理将 Derived** 转换为 Base** 的问题以及为什么它被禁止,我知道问题是你可以分配给 Base* 一些东西这不是Derived*,所以我们禁止这样做。

到目前为止,一切都很好。

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

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 不是完全按照您的要求回答了吗?
  • 不是真的,因为他们没有指出你可以用普通指针做同样的把戏

标签: c++ pointers inheritance


【解决方案1】:
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 对象的样子:

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

注意BaseDerived 有问题的对象都没有被尝试更改:只有中间指针是。

但是,当您只有一个间接级别时,代码会以对象允许的方式修改对象本身(它可以通过将 Base 中的赋值运算符设为私有、隐藏或删除来禁止它) :

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

【讨论】:

  • 是的,我认为我应该在我的心智模型中保留那个“子对象”。顺便说一句,您在“new and pointer”中的评论非常苛刻,因为它是用于显示概念的代码。
  • 不,我认为这是正确的哈希量。有太多的 Java 程序员在 C++ 的每一行都写new,这太傻了。
  • @akappa 使用现代 C++ 习语进行概念证明需要更少的时间来编写并且仍然是正确的,这与具有丑陋的 98 之前想法的概念证明不同。
  • 但是我这样做是因为我们在讨论指针转换,所以创建一个 Derived * 变量可以明确这一点。我本可以写一个Derived a; Derived *b = &amp;a,但我认为这样做很荒谬,因为“这里new 是无用的,应该像地狱一样避免”——我的意思是,即使我们对计算机进行编程,我们也应该保留一点灵活性为我们自己。
【解决方案2】:

不,nasty_function() 并不像听起来那么讨厌。当指针b 指向一个is-a Base 的东西时,给它分配一个Base-值是完全合法的。

请注意:您的“糟糕”评论不正确:d 仍然指向与通话前相同的 Derived!只是,它的 Base 部分被重新分配(按值!)。如果这使您的整个Derived 失去一致性,您需要通过使Base::operator=() 虚拟化来重新设计。然后,在nasty_function() 中,实际上将调用Derived 赋值运算符(如果已定义)。

所以,我认为,您的示例与指针到指针的情况没有太大关系。

【讨论】:

    【解决方案3】:

    *b = Base(3) 调用Base::operator=(const Base&amp;),它实际上存在于Derived 中,因为成员函数(包括运算符)被继承。

    然后会发生什么(调用Derived::operator=(const Base&amp;))有时称为"slicing",是的,这很糟糕(通常)。这是 C++ 中“变得类似”运算符(=)无处不在的一个可悲的结果。

    (请注意,“become-like”运算符在 Java、C# 或 Python 等大多数 OO 语言中不存在;= 在对象上下文中表示引用赋值,类似于 C++ 中的指针赋值;)。


    总结:

    强制转换 Derived** -> Base** 是被禁止的,因为它们会导致 类型错误,因为这样你最终可能会得到一个 Derived* 类型的指针指向一个类型的对象Base.

    你提到的问题不是类型错误;这是一种不同类型的错误:mis-use of interface derived 对象,源于它继承了其父类的“become-like”运算符这一令人遗憾的事实。 p>


    (是的,我故意在对象上下文中将 op= 称为“变得类似”,因为我觉得“赋值”并不是一个很好的名称来显示这里发生的事情。)

    【讨论】:

    • 是的,表面上它们看起来是同一个问题,但它们是完全不同的两个东西。谢谢!
    【解决方案4】:

    嗯,你给出的代码是有道理的。实际上,赋值运算符不能覆盖特定于 Derived 的数据,而只能覆盖基数。虚函数仍然来自 Derived 而不是来自 Base。

    【讨论】:

      【解决方案5】:
      *b = Base(3); // Ouch!
      

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

      您可能不希望修改基数,但就类型系统而言,它是正确的。 Derived Base

      这不适用于非法指针的情况。 Derived* 可转换为 Base* 但不是同一类型。它违反了类型系统。

      允许您询问的转换与此没有什么不同:

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

      【讨论】:

        【解决方案6】:

        通读我的问题的好答案,我想我明白了问题的重点,它来自 OO 中的第一原则,与子对象和运算符重载无关。

        关键是你可以在需要Base 时使用Derived(替换原则),但你不能在需要Base* 时使用Derived*,因为将派生类实例的指针分配给它的可能性。

        用这个原型做一个函数:

        void f(Base **b)
        

        f 可以用b 做很多事情,除其他外取消引用它:

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

        如果我们将Derived** 传递给f,则意味着我们将Derived* 用作Base*,这是不正确的,因为我们可以将OtherDerived* 分配给Base* 而不是@ 987654335@.

        另一方面,取这个函数:

        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的类集。

        【讨论】:

          猜你喜欢
          • 2018-12-15
          • 1970-01-01
          • 2014-03-27
          • 2017-08-06
          • 2019-02-06
          • 2010-12-01
          • 2010-11-24
          • 1970-01-01
          相关资源
          最近更新 更多