能够将子 class 实例分配给堆栈帧中的基础 class 变量的动机是什么?

What's the motivation for being able to assign a child class instance to base class variable in a stack frame?

鉴于此代码:

#include <iostream>

class base {
private:
    char x;
public:
    base(char x) : x(x) {
        std::cout << "base::base(char) " << this << std::endl;
    }
    base(base&& rhs) {
        std::cout << "base::base(base&&), moving from " << &rhs << " to " << this << std::endl;
        x = rhs.x; rhs.x = '[=12=]';
    }
    virtual ~base() {
        std::cout << "base::~base " << this << std::endl;
    }
};

class child : public base {
private:
    char y;
public:
    child(char x, char y) : base(x), y(y) {
        std::cout << "child::child(char, char) " << this << std::endl;
    }
    child(child&& rhs) : base(std::move(rhs)) {
        std::cout << "child::child(child&&), moving from " << &rhs << " to " << this << std::endl;
        y = rhs.y; rhs.y = '[=12=]';
    }
    virtual ~child() {
        std::cout << "child::~child " << this << std::endl;
    }
};

int main(int argc, char* argv[]) {
    { // This block enables me to read destructor calls on the console...
        base o = child('a', 'b');
    }
    std::cin.get();
    return 0;
}

main 的堆栈帧中有一个区域 child 正在被实例化。之后 base 的移动构造函数被调用并引用新创建的 childbase 的(移动)构造发生在同一堆栈帧的不同区域,并复制从 base 派生的 child 的部分。从现在开始,child 剩下的就是 base 部分了,只剩下 base.

我知道堆栈上的对象不可能实现多态性,我想我理解原因:效率。没有 vtable 查找等。编译器可以选择在编译时调用的方法。

我不明白的是:当构成 child 的所有内容时,为什么可以将 child 实例分配给 base 变量(在堆栈框架中)迷路?这样做的动机是什么?我期望编译错误或至少是警告,因为我想不出允许它的充分理由。

请记住 base o = child('a', 'b') 实际上是在调用复制构造函数,因此您并没有真正破坏 child 对象,而是从 [=13= 构造 o ] 作为副本。您需要使用 pointers/references 来对一个基础对象进行多态访问。

child c = child('a', 'b');
base o = c; // at this point we have two objects. 

其他语言,例如 Java 和 C# 具有类似的处理对象的语法,但它们实际上使用引用,因此与我们在这里所做的完全不同。

此外,在某些情况下,base 和 subclass 无论如何都具有相同的大小。一个激励案例是 class,它只是 int 的包装器,它公开了进行位操作以将内容打包到内部的成员。由于对象本身很小,因此按值移动它是有意义的。但是由于您可能只想查看原始 int,因此将其向下转换为更原始的类型是有意义的,并且不会丢失任何内容。

编辑: 好的,结果应该调用移动构造函数,但是 Visual Studio(我的编译器)还没有自动生成移动构造函数。

#include <iostream>
class Base
{
public:
    Base() {}
    Base(const Base& other) { std::cout << "Copy"; }
};

class Der : public Base{};

void main() {
    Base b = Der();
}

在Visual Studio中输出"Copy"。但是,如果您添加自己的移动构造函数,则会调用它,因为 Der() 是右值。

请记住,移动构造函数是 C++ 的新增功能,因此在 11 之前,该语法是允许的。由于它们必须支持遗留的 C++ 语法,因此它们实际上无法阻止它进行编译,仅仅因为它自从添加了移动构造函数以来并不总是那么有意义。这可能是问题的真正答案:遗留问题,因为在自动生成的移动构造函数中切片更值得商榷。

这对于评论来说太长了,实际上是答案的一部分:

我相信现在很明显您实际上是在创建一个新的 base 对象,而不是将一些现有值分配给变量(因为我们不在 Java 这里;C++获得 Java 行为的代码类似于 std::shared_ptr<base> o = std::make_shared<child>('a','b');)。另外根据评论,你现在正在寻找被允许的 why 的动机。答案非常简单:

因为 child 公开派生自 base,也称为具有“is-a”关系,任何 child 对象,从字面意义上讲,is a(特殊类型)base 对象。在设计良好的 class 层次结构中,您可以在任何需要 base 对象的地方使用 child 对象(并且 it will honor the interface invariants).这是一个重要的概念,这就是为什么它有一个名字:Liskov 替换原则。(这也是为什么你在布满灰尘的旧书中找到的关于 class 设计的例子有Rectangle derive from ShapeSquare derive from Rectangle 是非常糟糕的例子:在软件设计中,“is a” means/should 与它在软件设计中的含义不同数学。)

现在,您可以从 base 对象移动构造一个 base 对象,是吗?由于 child 对象 一个 base 对象,因此您也可以从那个 child 移动构造一个 base。 (当然,除非你违反通过从 base 公开派生而建立的契约。如果你愿意,C++ 会给你一把机关枪和胶带来擦掉你的脚。)