第16.4章 派生类中的五法则

English

1. 派生类的五个成员,都是由它的部件搭出来的

第14章列出了“拥有资源的类必须做对的五个操作”。 派生类里也是同样五个,而且每一个形状都一样:先对基类子对象做,再对每个成员做。 你不写的时候编译器就是这么生成的,而只要各部件行为正常,这个默认就是对的。

成员 它必须对基类做什么 契约
拷贝构造 拷贝构造基类子对象 只有当基类的拷贝是 noexcept 时才是
移动构造 移动构造基类子对象 只有当基类的移动是 noexcept 时才是
拷贝赋值 base::operator=(other) —— 显式调用 取决于基类的赋值
移动赋值 base::operator=(::std::move(other)) 取决于基类的移动赋值
析构函数 最后销毁基类子对象 永远是 noexcept
swap 把基类子对象当作 base& 交换 只有当基类的 swap 是 noexcept 时才是

其中三个是自动的。真正有意思的问题是哪些需要你自己写,而答案和 第14.1章一样: 基类和成员替你做不到的那几个。

2. “= default”不等于“忘记写”

什么都不写的派生类会得到全部五个:隐式定义,每一个都正确地处理基类子对象。 这就是零法则,而当基类与每个成员都行为良好时,它就是首选答案:


struct quiet_base
{
	::std::size_t id{};
};

struct rule_of_zero : quiet_base
{
	::std::size_t extra{};
};

rule_of_zero 可拷贝、可移动、可析构,而且它的移动是 noexcept —— 这一切都是编译器从 ::std::size_t 推出来的。 给五个成员各写一个 = default 什么都不会改变,而给每个都写函数体正是 bug 的来源。

编译器不会替你弥补坏掉的部件。如果基类删除了拷贝构造,派生类就没有拷贝构造, 而特征会如实报告:


struct no_copy_base
{
	::std::size_t id{};
	no_copy_base() noexcept = default;
	no_copy_base(no_copy_base const &) = delete;
};

struct derived_no_copy : no_copy_base
{
	::std::size_t extra{};
};

class guarded
{
	~guarded() noexcept = default;      // private destructor on purpose
};

struct derived_from_guarded : guarded
{
};

is_copy_constructible<derived_no_copy>=0
is_destructible<guarded>=0
is_destructible<derived_from_guarded>=0

第二、三行是同一条规则用在析构上:析构函数不可访问(这里是 private)的基类, 会让派生类的析构函数也被删除。这也是要保持基类析构函数 public 且 noexcept 的又一个理由。

3. 移动构造必须移动基类


derived(derived &&other) noexcept : base{::std::move(other)}, extra{other.extra}
{
	other.extra = 0;
}

::std::move(other) 的类型是 derived&&,而 derived&& 可以绑定到 base&& 形参上, 于是基类子对象由基类的移动构造函数移动构造 (第16.5章)。由此有两点:

而且要靠特征去验证,而不是靠记忆,因为 ::std::is_nothrow_move_constructible_v 回答的正是“整个对象”的这个问题 —— 基类加每一个成员。

4. 拷贝赋值:人人都忘的那一行

赋值运算符不能被继承,也不是从基类生成出来的:派生类的赋值要你自己写,而基类子对象也得你自己赋值。 忘掉那一行能编译、能运行,并且让对象的基类部分保持原样:


// the same class with the classic bug: the base is never assigned
struct buggy : base
{
	::std::int_least32_t extra{};

	explicit buggy(::std::size_t i, ::std::int_least32_t e) noexcept : base{i}, extra{e} {}

	buggy &operator=(buggy const &other) noexcept
	{
		extra = other.extra;             // base::operator= is missing
		return *this;
	}
};

buggy after assign: u.id=7 (expected 9) u.extra=90

u 被赋成了 v,成员被拷贝了,基类却留着旧的 id。没有警告、没有错误:基类子对象就是没参与这次赋值。 修法是两个赋值运算符里各加一行:


derived &operator=(derived const &other) noexcept
{
	base::operator=(other);
	extra = other.extra;
	return *this;
}

derived &operator=(derived &&other) noexcept
{
	base::operator=(::std::move(other));
	extra = other.extra;
	other.extra = 0;
	return *this;
}

关于层次结构里的赋值,还有两条规则:

5. swap 与拷贝并交换

派生类的 swap 和其他成员是同一个两步模式:先交换基类,再交换成员。 基类必须写成 base&,这样重载解析才能找到基类的 swap, 而不是递归回自己:


void swap(derived &other) noexcept
{
	using ::std::swap;
	swap(static_cast<base &>(*this), static_cast<base &>(other));
	swap(extra, other.extra);
}

有了这两行,第14.8章的 “拷贝并交换”赋值对派生类原样适用,而且保留了强异常安全保证: 新值在触碰任何东西之前就已经完整构造好。

6. 完整程序


#include <fast_io.h>
#include <cstddef>
#include <cstdint>
#include <utility>
#include <type_traits>

struct base
{
	::std::size_t id{};

	base() noexcept = default;
	explicit base(::std::size_t i) noexcept : id{i} {}

	base(base const &) noexcept = default;
	base(base &&other) noexcept : id{other.id} { other.id = 0; }
	base &operator=(base const &) noexcept = default;
	base &operator=(base &&other) noexcept
	{
		id = other.id;
		other.id = 0;
		return *this;
	}
	~base() noexcept = default;
};

// the derived class that does it right
struct derived : base
{
	::std::int_least32_t extra{};

	derived() noexcept = default;
	explicit derived(::std::size_t i, ::std::int_least32_t e) noexcept : base{i}, extra{e} {}

	derived(derived const &) noexcept = default;

	derived(derived &&other) noexcept : base{::std::move(other)}, extra{other.extra}
	{
		other.extra = 0;
	}

	derived &operator=(derived const &other) noexcept
	{
		base::operator=(other);          // the line everybody forgets
		extra = other.extra;
		return *this;
	}

	derived &operator=(derived &&other) noexcept
	{
		base::operator=(::std::move(other));
		extra = other.extra;
		other.extra = 0;
		return *this;
	}

	~derived() noexcept = default;

	void swap(derived &other) noexcept
	{
		using ::std::swap;
		swap(static_cast<base &>(*this), static_cast<base &>(other));
		swap(extra, other.extra);
	}
};

// the same class with the classic bug: the base is never assigned
struct buggy : base
{
	::std::int_least32_t extra{};

	explicit buggy(::std::size_t i, ::std::int_least32_t e) noexcept : base{i}, extra{e} {}

	buggy &operator=(buggy const &other) noexcept
	{
		extra = other.extra;             // base::operator= is missing
		return *this;
	}
};

int main()
{
	try
	{
		derived a{1, 10};
		derived b{::std::move(a)};
		::fast_io::io::println("after move: b.id=", b.id, " b.extra=", b.extra, " a.id=", a.id, " a.extra=", a.extra);

		derived c{2, 20};
		c = b;
		::fast_io::io::println("after copy assign: c.id=", c.id, " c.extra=", c.extra);

		buggy u{7, 70};
		buggy v{9, 90};
		u = v;
		::fast_io::io::println("buggy after assign: u.id=", u.id, " (expected 9) u.extra=", u.extra);

		derived x{3, 30};
		derived y{4, 40};
		x.swap(y);
		::fast_io::io::println("after swap: x.id=", x.id, " y.id=", y.id);
	}
	catch throws (::std::error e)
	{
		::fast_io::io::perrln(e);
	}
}

输出:


after move: b.id=1 b.extra=10 a.id=0 a.extra=0
after copy assign: c.id=1 c.extra=10
buggy after assign: u.id=7 (expected 9) u.extra=90
after swap: x.id=4 y.id=3

四行输出,四个教训。移动取走了两半、也清空了两半。拷贝赋值取走了两半。有 bug 的赋值只取走一半。 交换则把基类子对象和成员都换了过来。

7. 契约:派生构造函数继承了所有失败可能

8. 经验法则