第16.5章 派生到基类的转换与截断

English

1. 派生对象就是一个基类对象

因为基类子对象是派生对象真实的一部分 (第16.1章), 所以在需要基类对象的地方可以直接使用派生对象,不需要任何转换:

目标 从一个 derived 左值出发 发生了什么
base& base& r = d; 绑定到 d 内部的基类子对象;不拷贝
base const& base const& r = d; 同上,只读
base* base* p = &d; 指向基类子对象;不拷贝
base base b = d; 只拷贝基类那部分 —— 这就是截断

前三种是有用的:免费、保留对象本身,而且通过引用写入会落到原对象上。第四种是陷阱, 也是本页的主题。

2. 截断:派生部分是会被扔掉的

base b = d; 不是“转换”d,而是用 d 的基类子对象 构造一个新的 base。新对象的大小和成员都是 base 的, 所以派生类新增的一切都消失了 —— 悄无声息,没有警告,因为对编译器来说这只是一次普通的 拷贝构造调用。


#include <fast_io.h>
#include <cstdint>

struct base
{
	::std::int_least32_t v{};
};

struct derived : base
{
	::std::int_least32_t extra{};
};

int main()
{
	try
	{
		derived d{};
		d.v = 1;
		d.extra = 2;

		base sliced = d;                       // copies only the base subobject
		::fast_io::io::println("sliced.v=", sliced.v, " sizeof(sliced)=", sizeof(sliced));

		base &ref = d;                         // refers to the base subobject inside d
		ref.v = 5;
		::fast_io::io::println("after ref.v=5: d.v=", d.v, " d.extra=", d.extra);
	}
	catch throws (::std::error e)
	{
		::fast_io::io::perrln(e);
	}
}

输出:


sliced.v=1 sizeof(sliced)=4
after ref.v=5: d.v=5 d.extra=2

sizeof(sliced) 是 4:那个对象是一个 base, 不是 derived。相反,ref 是真实的基类子对象 —— 写 ref.v 改变了 d.v,而 d.extra 没被动过, 因为它从头到尾没参与。

3. 你没写也会发生的截断

截断不是冷门操作。每当派生对象被拷贝进一个基类类型的值里,它就发生了:

在没有 virtual 函数的层次结构里,这些至少还是类型诚实的: 一个 base 值确实是 base,对它的每个操作都是基类的操作。 一旦基类有了虚函数,截断还会变成逻辑 bug,因为那份拷贝不再表现得像原对象 —— 第18章会讲这件事。

4. 传引用,不要传值

由此得到的规则只有一行:形参列表里的基类永远是引用或指针,绝不是值。


inline void report(base const &b) throws;      // uses the object you passed
inline void report(base b) throws;              // uses a copy of its base subobject

inline void mutate(base &b) noexcept;          // writes reach the original

这也是 fast_io 的例子都用引用传设备的原因: ::fast_io::io::println(out, ...) 用转发引用接收输出设备, 所以 obuf_file 仍然是 obuf_file,它的缓冲区不会被拷贝 (第11.1章)。

5. 经验法则