第17.9章 规则与检查表

English

1. 按顺序做的判断

当你准备写一个运算符时,按下面的顺序问,遇到第一个“是”就停:

  1. 这个操作会失败、会分配、会阻塞,或者需要报告错误吗? 那它就是带 throws 契约的命名函数 (第17.2章)。
  2. 标点的日常含义恰好就是这个操作吗?如果不是 —— 用 / 连接路径、用 | 求并、用 << 格式化 —— 它就是命名函数(第17.8章)。
  3. 它是 && || , & 之一,或者一个转换运算符吗? 那它不存在:explicit operator bool 是唯一的例外。
  4. 这个类型建模的是一个数吗? 那就用算术运算符、默认的 == 和 <=>, 全部 noexcept、全部 [[nodiscard]] (第17.3章、 第17.4章)。
  5. 这个操作是一次不会失败的普通容器访问吗? 那就 operator[];否则用命名的 at / find (第17.7章)。
  6. 其余情况 —— 用一个名字。

2. 一个通过了检查表的类型

config 有相等,因为相等有意义而且免费;别的什么都没有。 真正干活的两个操作是函数,而它们的名字携带了运算符会隐藏掉的信息: with_port 返回一个修改过的副本,validate 会失败。


#include <fast_io.h>
#include <fast_io_dsal/string.h>
#include <cstdint>

struct config
{
	::fast_io::string name{};
	::std::int_least32_t port{};

	/* equality: yes, and the compiler writes it */
	friend bool operator==(config const &, config const &) = default;

	/* ordering: no. Two configs are not "less" than each other. */
	/* arithmetic: no. Adding two configurations is not a thing. */
	/* stream operator: no. Formatting is print_define's job. */
	/* conversion operator: no. Use named accessors. */
	/* operator new: no. See Ch18.7. */

	[[nodiscard]] constexpr bool is_valid() const noexcept
	{
		return port > 0 && port < 65536;
	}
};

/*
The two operations that exist are named, so the reader knows a copy is made and that
validation has a contract.
*/
[[nodiscard]] inline config with_port(config c, ::std::int_least32_t port)
{
	c.port = port;
	return c;
}

inline void validate(config const &c) throws
{
	if (!c.is_valid())
	{
		::fast_io::herbceptions::throws_errc(::std::errc::invalid_argument);
	}
}

int main()
{
	try
	{
		config const http{::fast_io::string{"http"}, 80};
		config const alt{with_port(http, 8080)};
		::fast_io::io::println("http == alt: ", http == alt, "   alt.port = ", alt.port);
		validate(alt);
		::fast_io::io::println("alt is valid: ", alt.is_valid());
	}
	catch throws (::std::error e)
	{
		::fast_io::io::print("error: ");
		::fast_io::io::println(e);
	}
}

http == alt: 0   alt.port = 8080
alt is valid: 1

注意这个类型缺少什么:没有 operator+(把两个配置相加没有意义)、 没有 operator<(两个配置之间没有序)、没有 operator<< (格式化是 print_define 的事)、没有转换运算符、没有 operator new。 其中两条“缺少”就是整个设计:它们把“可能有人会误用”变成了编译错误。

3. 把检查表变成 grep

只存在于文档里的规则在真实代码库里活不下去。下面这些是机械的,每一条都值得一个 CI 任务:

检查 它能找出什么
grep -rn "operator&&\|operator||\|operator,\|operator&(" 那四个永远不该被重载的运算符
grep -rn "operator<<\|operator>>" 流运算符,包括从老代码复制进新代码的那些
grep -rn "operator new\|operator delete" 类级分配(第18.7章)
grep -rn "operator .*()" | grep -v explicit 隐式转换
grep -rn "operator+" | grep -v noexcept 没有契约的值运算符

最后一条最小,也最有用:在这本书里每个函数都有错误契约 (第6.16章), 一个既不是 noexcept 也不是 throws 的运算符, 说明它没有被想过。

4. 小结