第17.9章 规则与检查表
1. 按顺序做的判断
当你准备写一个运算符时,按下面的顺序问,遇到第一个“是”就停:
- 这个操作会失败、会分配、会阻塞,或者需要报告错误吗?
那它就是带
throws契约的命名函数 (第17.2章)。 - 标点的日常含义恰好就是这个操作吗?如果不是 ——
用
/连接路径、用|求并、用<<格式化 —— 它就是命名函数(第17.8章)。 - 它是
&& || , &之一,或者一个转换运算符吗? 那它不存在:explicit operator bool是唯一的例外。 - 这个类型建模的是一个数吗?
那就用算术运算符、默认的
==和<=>, 全部noexcept、全部[[nodiscard]](第17.3章、 第17.4章)。 - 这个操作是一次不会失败的普通容器访问吗?
那就
operator[];否则用命名的at/find(第17.7章)。 - 其余情况 —— 用一个名字。
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. 小结
- 运算符就是一个名字是标点的函数。这个名字携带的信息比任何标识符都少, 所以这个操作必须显然、全域、免费。
- 优先用命名函数。
concat和append说出了+说不出的东西:是否产生新值、是否有东西被拷贝。 - 先定义复合赋值;拿它定义二元运算符;两个操作数顺序都提供;
结果标
[[nodiscard]]。 ==和<=>用= default; 序不是全序时,在命名函数里用partial_ordering。operator[]是算术,不能失败;其余的一律要有名字。- 永远不要重载
&&、||、,、&、<<、>>或operator new。 explicit operator bool是你唯一该拥有的转换运算符。