第16.2章 构造与析构顺序

English

1. 顺序是固定的,而且不是初始化列表的顺序

构造一个派生对象分三个阶段,顺序由语言规定:

  1. 基类子对象被构造,按基类列出的顺序;
  2. 成员被构造,按它们在类中声明的顺序 —— 不是初始化列表提到它们的顺序(第13.6章);
  3. 派生类构造函数的函数体执行。

析构完全相反:函数体作用域结束,然后成员按声明逆序销毁,最后是基类。这就是为什么派生类的 析构函数可以读基类的成员:它们还活着。而基类的析构函数绝不能碰派生类新增的东西 —— 那些已经没了。

2. 初始化基类

如果基类有默认构造函数,派生类不必提到它。如果没有,派生类构造函数必须在初始化 列表里点名:


class machine_part
{
protected:
	::std::int_least32_t serial_{};

public:
	machine_part() noexcept = default;
	explicit machine_part(::std::int_least32_t s) noexcept : serial_{s} {}
};

class machine : public machine_part
{
	::std::int_least32_t power_{};

public:
	machine(::std::int_least32_t serial, ::std::int_least32_t power) noexcept
		: machine_part{serial}, power_{power}
	{
	}
};

基类是用它的类型点名的,不是用某个成员名: machine_part{serial}。写成 serial_{serial} 是错误 —— serial_ 属于基类子对象,而基类子对象要通过它自己的构造函数初始化。

3. 析构函数不能打印

第6.16章说每个函数要么 noexcept 要么 throws,而析构函数永远是 noexcept。打印会失败(管道被关、磁盘写满),所以会打印的析构函数会被编译器 拒绝。它说的是:


error: call to a ‘throws’ or ‘return_failure{E}’ function must be handled by an enclosing
       ‘try { } catch throws’, or the calling function needs a herbceptions specification
       so the error can propagate

这条信息很精确:noexcept 函数不能调用 throws 函数除非它自己 处理失败。光这条诊断就是教训,但修法也要讲,所以下面的例子把事件记进一个小缓冲区 (写入函数是 noexcept),之后在 main 里把轨迹打印出来 —— 那里允许失败。

4. 观察顺序


namespace trace
{
inline ::std::string_view events[8]{};
inline ::std::size_t count{};

inline void add(::std::string_view e) noexcept
{
	if (count != 8)
	{
		events[count++] = e;
	}
}
} // namespace trace

struct base
{
	base() throws { trace::add("base()"); }
	~base() noexcept { trace::add("~base()"); }
};

struct engine
{
	engine() throws { trace::add("  engine()"); }
	~engine() noexcept { trace::add("  ~engine()"); }
};

struct machine : base
{
	engine eng{};
	::std::string_view tag{};

	machine() throws : base{}, tag{"built"} { trace::add("machine()"); }
	~machine() noexcept { trace::add("~machine()"); }
};

注意两个契约:构造函数是 throws,因为可能失败的派生构造函数必须被允许向上 传播;析构函数是 noexcept,所以只碰一个固定数组。如果基类构造函数抛出, 派生对象根本不存在,只有那些已经完整构造好的部分才会被析构。

5. 完整程序


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

namespace trace
{
inline ::std::string_view events[8]{};
inline ::std::size_t count{};

inline void add(::std::string_view e) noexcept
{
	if (count != 8)
	{
		events[count++] = e;
	}
}
} // namespace trace

struct base
{
	base() throws { trace::add("base()"); }
	~base() noexcept { trace::add("~base()"); }
};

struct engine
{
	engine() throws { trace::add("  engine()"); }
	~engine() noexcept { trace::add("  ~engine()"); }
};

struct machine : base
{
	engine eng{};
	::std::string_view tag{};

	machine() throws : base{}, tag{"built"} { trace::add("machine()"); }
	~machine() noexcept { trace::add("~machine()"); }
};

int main()
{
	try
	{
		{
			machine m{};
			::fast_io::io::println("in scope: ", m.tag);
		}
		::fast_io::io::println("out of scope");

		for (::std::size_t i{}; i != trace::count; ++i)
		{
			::fast_io::io::println(trace::events[i]);
		}
	}
	catch throws (::std::error e)
	{
		::fast_io::io::perrln(e);
	}
}

输出:


in scope: built
out of scope
base()
  engine()
machine()
~machine()
  ~engine()
~base()

把最后六行读成对称的两半。构造:基类、成员、函数体。析构:函数体、成员逆序、基类。 tag 成员在 machine() 之前构造,所以函数体已经可以依赖它。

6. 人们常搞错的两个后果

7. 经验法则