第16.8章 继承还是组合?

English

1. 复用有两种写法

车有一台发动机。把它写成继承,得到的句子是“车是发动机”,这是假的,而这个假有后果: 每个 engine& 形参都会接受一辆车,一辆车不能有两台发动机, 而改动 engine 会悄悄改变 car 的公开接口。


struct engine
{
	::std::int_least32_t power{};
};

struct car_inherits : engine          // "a car is an engine"
{
	::std::string_view model{};
};

struct car_has                    // "a car has an engine"
{
	engine eng{};
	::std::string_view model{};
};

两者在这台机器上一样大(24 字节:一个 ::std::int_least32_t 加一个 ::std::string_view,凑到 8 的倍数),存的数据也一样。不同的是类型关系,而价值全在那里。

2. 组合买到了什么

继承 组合
is-a 断言的正确性 必须字面为真 根本不断言
能否装两台发动机 不能 能,再加一个成员
部件接口变化时 整体接口跟着变,可能还动 ABI 被包住了;只有包装类自己的成员才算数
访问控制 继承来的成员按名字就能用 只能通过成员名用,包装类因此能设关卡
运行期换实现 不行(基类子对象换不掉) 可以,如果成员是 variant 或擦除类型
sizeof 与部件加上新成员相同 相同,除非部件是空类型(§4)

倒数第二行要记住:成员是一道边界。每个派生类永远待在基类的不变式里; 每个持有成员的人都能决定公开什么。

3. 继承才是对的答案的情形

当下面四条全部成立时,继承才配得上它的位置:

  1. is-a 那句话对每个调用者都成立,
  2. 基类就是设计来被继承的(有文档、有正确的析构函数、该 protected 的地方 protected),
  3. 派生类增加的是行为,不只是数据,
  4. 派生类型集合要么封闭且已知,要么基类真的是一个接口。

本章自己的例子都通过:labeled_point 是一个点,machine 是一个 machine_part。车在第一条就失败了,再怎么共享实现也救不回来。

4. 空基类:继承在尺寸上真正赢的唯一地方

有一种情况下继承在 sizeof 上胜过成员:空基类不占字节,而空成员仍然必须可寻址 —— 至少要一个字节,再加填充。


struct empty
{
};

struct with_member
{
	empty e{};
	::std::int_least32_t x{};
};

struct with_member_no_unique
{
	[[no_unique_address]] empty e{};
	::std::int_least32_t x{};
};

struct derived : empty
{
	::std::int_least32_t x{};
};
类型 sizeof 原因
empty 1 每个对象都需要唯一地址
derived 4 空基类优化:基类不占空间
with_member 8 成员必须有自己的地址,于是 1 字节 + 3 填充
with_member_no_unique 4 [[no_unique_address]](C++20)给了成员同样的许可

C++20 之后尺寸这条理由也没了:[[no_unique_address]] 让空成员免费, 所以组合不再花字节。注意它没有解决的问题:类型关系、两台发动机的问题、接口的 containment。 尺寸从来都不是有意思的部分。

5. 第三种选择:完全没有关系

当层次结构的理由是“这些东西必须被统一处理”时,继承和成员都不是现代答案:

继承是这张清单里第四种情况的正解,对前三种都是错解。fast_io 自己的容器就是这样: 它们不共享基类,它们是模板,而真的需要一个公共类型时,库选择擦除而不是搬出一个基类。

6. 完整程序


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

struct engine
{
	::std::int_least32_t power{};
};

struct car_inherits : engine
{
	::std::string_view model{};
};

struct car_has
{
	engine eng{};
	::std::string_view model{};
};

struct empty
{
};

struct with_member
{
	empty e{};
	::std::int_least32_t x{};
};

struct with_member_no_unique
{
	[[no_unique_address]] empty e{};
	::std::int_least32_t x{};
};

struct derived : empty
{
	::std::int_least32_t x{};
};

int main()
{
	try
	{
		car_inherits a{};
		car_has b{};
		a.power = 120;
		b.eng.power = 120;

		::fast_io::io::println("inherits: ", a.power, " sizeof=", sizeof(car_inherits));
		::fast_io::io::println("has:      ", b.eng.power, " sizeof=", sizeof(car_has));
		::fast_io::io::println("sizeof(empty)=", sizeof(empty), " sizeof(derived)=", sizeof(derived));
		::fast_io::io::println("sizeof(with_member)=", sizeof(with_member),
							   " sizeof(with_member_no_unique)=", sizeof(with_member_no_unique));
	}
	catch throws (::std::error e)
	{
		::fast_io::io::perrln(e);
	}
}

输出:


inherits: 120 sizeof=24
has:      120 sizeof=24
sizeof(empty)=1 sizeof(derived)=4
sizeof(with_member)=8 sizeof(with_member_no_unique)=4

7. 经验法则