第16章 实用工具
第16章 实用工具
- 介绍
- 时间
- 时钟;
- 日历;
- 时区
- 函数适配
- Lambda作为适配器;
- mem_fn() ;
- function
- 类型函数
- 类型谓词;
- 条件属性;
- 类型生成器;
- 关联类型
- source_location
- move() 和 forward()
- 位操作
- 退出程序
- 建议
16.1 介绍
将库组件标记为“实用工具”并不是很有信息量。显然,在某个时间点,每个库组件都对某人、在某处具有实用性。此处选择的设施是因为它们对许多人来说都至关重要,但其描述并不适合放在其他地方。通常,它们作为更强大的库设施(包括标准库的其他组件)的构建块。
16.2 时间
在 <chrono> 中,标准库提供了处理时间的设施:
• 时钟、 time_point 和持续时间 duration ,用于测量某些操作花费的时间,并作为与时间相关的任何操作的基础。
• day 、 month 、 year 和 weekdays ,用于将 time_point 映射到我们的日常生活中。
• time_zone (时区)和 zoned_time (带时区的时间),用于处理全球各地时间报告的差异。 基本上,每个主要系统都会处理其中的一些实体。
16.2.1 时钟
以下是对某些操作进行计时的基本方式:
using namespace std::chrono; // 在子命名空间std::chrono中 §3.3
auto t0 = system_clock::now();
do_work();
auto t1 = system_clock::now();
cout << t1-t0 << "\n"; //102593[1/10000000]s
cout << duration_cast<milliseconds>(t1-t0).count() << "ms\n"; //10ms
cout << duration_cast<nanoseconds>(t1-t0).count() << "ns\n"; //10259300ns
时钟返回一个 time_point (即时间中的一个点)。两个时间点相减会得到一个 duration (即一段时间)。对于 duration ,默认的 << 操作符会在其后添加一个表示所用单位的后缀。各种时钟会以各种时间单位(“时钟滴答”)给出其结果(我使用的时钟以一百纳秒为单位进行测量),因此,将 duration 转换为适当的单位通常是一个好主意。这就是 duration_cast 的作用。
时钟对于快速测量很有用。在没有进行时间测量之前,不要对代码的“效率”发表评论。对性能的猜测是最不可靠的。快速、简单的测量比没有测量要好,但现代计算机的性能是一个棘手的话题,因此我们必须小心,不要过于重视一些简单的测量。为了降低被罕见事件或缓存效应蒙蔽的可能性,应始终进行重复测量。
命名空间 std::chrono_literals 定义了时间单位后缀(§6.6)。例如:
this_thread::sleep_for(10ms+33us); // 等待10毫秒又33微秒
使用常规符号名称可以大大提高可读性,并使代码更易于维护。
16.2.2 日历
在处理日常事件时,我们很少使用毫秒;我们使用年、月、日、时、分、秒和星期几。标准库支持这些操作。例如:
auto spring_day = April/7/2018;
cout << weekday(spring_day) << '\n';
// Sat
cout << format("{:%A}\n",weekday(spring_day));
// Saturday
Sat 是我的电脑上星期六的默认字符表示。我不喜欢这种缩写,所以我使用 format (§11.6.2) 来获取更长的名称。出于某种不明显的原因, %A 表示“写出星期几的全名”。当然, April 是一个月份;更准确地说,是一个 std::chrono::Month 。我们也可以说
auto spring_day = 2018y/April/7;
y 后缀用于区分年份和普通的 int ,后者用于表示编号为 1 到 31 的月份中的天数。
可以表示无效的日期。如果有疑问,请使用 ok() 进行检查:
auto bad_day = January/0/2024;
if (!bad_day.ok())
cout << bad_day << " is not a valid day\n";
显然,对于通过计算获得的日期, ok() 最有用。
日期是通过重载 year 、 month 和 int 类型的运算符 / 来组成的。所得的 Year_month_day 类型与时间点 time_point 之间可相互转换,从而允许进行准确高效的日期计算。例如:
sys_days t = sys_days{February/25/2022}; // 获取一个具有天数精度的时间点
t += days{7}; // 2022 年 2 月 25 日之后的一周
auto d = year_month_day(t); // 将时间点转换回日历
cout << d << '\n'; //2022-03-04
cout << format("{:%B}/{}/{}\n", d.month(), d.day(), d.year()); // March/04/2022
此计算需要更改月份并了解闰年。默认情况下,实现会以 ISO 8601 标准格式给出日期。为了将月份拼写为“March”,我们必须分解日期的各个字段并进入格式设置细节(§11.6.2)。出于某种不明显的原因, %B 表示“写出月份的全名”。 此类操作通常可以在编译时完成,因此速度非常快:
static_assert(weekday(April/7/2018) == Saturday); // true
日历是复杂而微妙的。这对于为“普通人”而非程序员设计的、旨在简化编程的“系统”而言是典型且适当的。标准库中的日历系统可以(并且已经)扩展以处理儒略历、伊斯兰历、泰国历和其他历法。
16.2.3 时区
与时间相关的一个最棘手的问题是时区。时区非常随意,很难记住,而且它们在全球各地没有统一标准的情况下,会以各种方式发生变化。例如:
#include "pch.h";
#include <iostream>;
#include <chrono>;
using namespace std;
using namespace std::chrono;
int main()
{
auto tp = system_clock::now();
cout << tp << '\n';//2024-07-02 14:18:34.4153502
zoned_time ztp{ current_zone(),tp };
cout << ztp << '\n';//2024-07-02 22:18:34.4153502 GMT+8
time_zone const* est = locate_zone("Europe/Copenhagen");
cout << zoned_time{ est, tp } << '\n';//2024-07-02 16:18:34.4153502 GMT+2
}
time_zone 是相对于 system_clock 使用的标准(称为GMT或UTC)的时间。标准库与全球数据库(IANA)同步,以获得正确的答案。这种同步可以在操作系统中自动进行,或者在系统管理员的控制下进行。时区的名称是C风格的字符串,格式为“大陆/主要城市”,例如“ Asia/Shanghai” 、“ America/New_York ”、“ Africa/Nairobi ”。 zoned_time 是一个 time_zone 与一个 time_point 的组合。
与时区一样,日历也解决了一系列我们应该留给标准库去处理的问题,而不是依赖我们自己手工编写的代码。考虑一下:2024年2月的最后一天,纽约的几点钟,新德里的日期会发生变化?2020年美国科罗拉多州丹佛市的夏令时(夏令时)什么时候结束?下一个闰秒什么时候出现?标准库“知道”这些问题的答案。
16.3 函数适配
当将一个函数作为函数参数传递时,参数的类型必须与被调用函数声明中所表达的期望完全匹配。如果预期的参数只是“几乎符合期望”,我们有其他方法来调整它:
• 使用 lambda 表达式(§16.3.1)。
• 使用 std::mem_fn() 从成员函数创建一个函数对象(§16.3.2)。
• 定义函数以接受 std::function (§16.3.3)。
还有许多其他方法,但通常这三种方法之一效果最好。
16.3.1 Lambda作为适配器
考虑经典的“绘制所有形状”示例:
void draw_all(vector<Shape*>& v)
{
for_each(v.begin(),v.end(),[](Shape* p) { p->draw(); });
}
像所有标准库算法一样, for_each() 使用传统的函数调用语法 f(x) 调用其参数,但 Shape 的 draw() 使用常规的面向对象语法 x->f() 。Lambda轻松地在两种语法之间进行转换。
16.3.2 mem_fn()
给定一个成员函数,函数适配器 mem_fn(mf) 生成一个可以作为非成员函数调用的函数对象。例如:
void draw_all(vector<Shape*>& v)
{
for_each(v.begin(),v.end(),mem_fn(&Shape::draw));
}
在C++11引入 lambda 表达式之前, mem_fn() 及类似功能是主要的手段,用于将面向对象的调用风格映射到函数式调用风格上。
16.3.3 function
标准库中的 function 是一种类型,可以持有任何你可以使用调用运算符 () 调用的对象。也就是说, function 类型的对象是一个函数对象(§7.3.2)。例如:
int f1(double);
function<int(double)> fct1 {f1}; // 初始化为f1
int f2(string);
function fct2 {f2}; // fct2的类型是function<int(string)>
function fct3 = [](Shape* p) { p->draw(); }; // fct3的类型是function<void(Shape*)>
对于 fct2 ,我让函数的类型从初始化器中推导出来: int(string) 。
显然, function 对于回调、作为参数传递操作、传递函数对象等都非常有用。然而,与直接调用相比,它可能会引入一些运行时开销。特别是,对于编译时未计算大小的函数对象,可能会发生自由存储分配,这对性能敏感的应用程序有严重的不良影响。C++23将提供一个解决方案: move_only_function 。
另一个问题是, function 作为对象,不参与重载。如果你需要重载函数对象(包括 lambda ),请考虑使用 overloaded (§15.4.1)。
16.4 类型函数
类型函数 是一种在编译时进行求值的函数,它以类型作为其参数或返回一个类型。标准库提供了各种类型函数,以帮助库实现者(以及一般程序员)编写能够利用语言、标准库和一般代码特性的代码。 对于数值类型, <limits> 中的 numeric_limits 提供了各种有用的信息(§17.7)。例如:
constexpr float min = numeric_limits<float>::min(); // 最小的正浮点数
同样,可以使用内置的 sizeof 运算符(§1.4)来查看对象的大小。例如:
constexpr int szi = sizeof(int); // int类型的字节数
在 <type_traits> 中,标准库提供了许多用于查询类型属性的函数。例如:
bool b = is_arithmetic_v<X>; // 如果X是(内置)算术类型之一,则为true
using Res = invoke_result_t<decltype(f)>; // 如果f是返回int的函数,则Res是int
decltype(f) 是对内置类型函数 decltype() 的调用,它返回其参数 f 的声明类型。
一些类型函数基于输入创建新类型。例如:
typename<typename T>
using Store = conditional_t(sizeof(T)<max, On_stack<T>, On_heap<T>);
如果 conditional_t 的第一个(布尔)参数为 true ,则结果为第一个备选方案;否则,为第二个。假设 On_stack 和 On_heap 为 T 提供相同的访问函数,它们可以根据其名称指示的方式为 T 分配内存。因此,可以根据 X 对象的大小来调整 Store<X> 的用户。这种分配选择所带来的性能调整可能非常重要。 这是一个简单的例子,说明了我们如何可以从标准类型函数或使用概念来构造自己的类型函数。 概念是类型函数。当在表达式中使用时,它们是特定的类型谓词。例如:
template<typename F, typename... Args>
auto call(F f, Args... a, Allocator alloc)
{
if constexpr (invocable<F,alloc,Args...>) // 需要分配器吗?
return f(f,alloc,a...);
else
return f(f,a...);
}
在许多情况下,概念是最好的类型函数,但标准库的大部分是在概念之前编写的,并且必须支持概念之前的代码库。
符号约定令人困惑在标准库中,通常使用后缀 _v 来命名那些返回值的类型函数,而使用后缀 _t 来命名那些返回类型的类型函数。这是C语言弱类型时代和概念之前C++的遗留用法。没有标准库类型函数同时返回类型和值,因此这些后缀是多余的。在标准库和其他地方使用概念时,不需要也不使用后缀。
类型函数是C++编译时计算机制的一部分,允许比没有它们时更严格的类型检查和更好的性能。类型函数和概念(第8章,§14.5)的使用通常被称为 元编程 或 模板元编程 (当涉及模板时)。
16.4.1 类型谓词
在 <type_traits> 中,标准库提供了数十个简单的类型函数,称为 类型谓词 ,用于回答关于类型的基本问题。以下是一些示例:
is_void_v<T> | T 是否为void类型? |
is_integral_v<T> | T 是否为整数类型? |
is_floating_point_v<T> | T 是否为浮点类型? |
is_class_v<T> | T 是否为类类型(而非联合体)? |
is_function_v<T> | T 是否为函数类型(而非函数对象或函数指针)? |
is_arithmetic_v<T> | T 是否为整数或浮点类型? |
is_scalar_v<T> | T 是否为算术类型、枚举类型、指针类型或成员指针类型? |
is_constructible_v<T, A...> | 是否可以从参数列表 A... 构造 T ? |
is_default_constructible_v<T> | T 是否可以默认构造(即无参构造)? |
is_copy_constructible_v<T> | T 是否可以从另一个 T 拷贝构造? |
is_move_constructible_v<T> | T 是否可以从另一个 T 移动构造? |
is_assignable_v<T,U> | U 是否可以直接赋值给 T ? |
is_trivially_copyable_v<T> | 在没有用户自定义拷贝操作时 U 是否可以赋值给 T ? |
is_same_v<T,U> | T 是否与 U 是同一类型? |
is_base_of_v<T,U> | U 是否派生自 T ,或 U 是否与 T 同类型? |
is_convertible_v<T,U> | T 是否能隐式转换为 U ? |
is_iterator_v<T> | T 是否为迭代器类型? |
is_invocable_v<T, A...> | T 是否能使用参数列表 A... 被调用? |
has_virtual_destructor_v<T> | T 是否有虚析构函数? |
这些谓词的一种传统用途是约束模板参数。例如:
template<typename Scalar>
class complex {
Scalar re, im;
public:
static_assert(is_arithmetic_v<Scalar>, "抱歉,我只支持算术类型的复数");
// ...
};
然而,与其他传统用法一样,使用概念可以更简单、更优雅地完成这一任务:
template<Arithmetic Scalar>
class complex {
Scalar re, im;
public:
// ...
};
在许多情况下,如 is_arithmetic 这样的类型谓词会消失,取而代之的是更易于使用的概念定义。例如:
template<typename T>
concept Arithmetic = is_arithmetic_v<T>;
有趣的是,并没有 std::arithmetic 概念。通常,我们可以定义比标准库类型谓词更通用的概念。
许多标准库类型谓词仅适用于内置类型。我们可以根据所需的操作来定义概念,如 Number 的定义(§8.2.4)所示:
template<typename T, typename U = T>
concept Arithmetic = Number<T,U> && Number<U,T>;
最常见的是,标准库类型谓词的使用深藏在实现基本服务的代码中,通常用于区分优化情况。例如, std::copy(Iter,Iter,Iter2) 的部分实现可以对简单类型(如整数)的连续序列进行重要优化:
template<class T>
void cpy1(T* first, T* last, T* target)
{
if constexpr (is_trivially_copyable_v<T>)
memcpy(first, target, (last - first) * sizeof(T));
else
while (first != last) *target++ = *first++;
}
在一些实现中,这种简单的优化比其非优化变体快约50%。
除非你已经验证标准库没有提供更好的方案,否则不要沉迷于这种小聪明。手动优化的代码通常比更简单的替代方案更难维护。
16.4.2 条件属性
考虑定义一个“智能指针”:
template<typename T>
class Smart_pointer {
// ...
T& operator*() const;
T* operator->() const; // -> 只有在T是类时才工作
};
-> 应该在且仅在 T 是类类型时定义。例如, Smart_pointer<vector<T>> 应该有 -> ,但 Smart_pointer<int> 不应该有。 我们不能使用编译时 if ,因为我们不在函数内部。相反,我们这样写:
template<typename T>
class Smart_pointer {
// ...
T& operator*() const;
T* operator->() const requires is_class_v<T>; // -> 只有在T是类时才定义
};
类型谓词直接表示对 operator->() 的约束。我们也可以使用概念来实现这一点。没有标准库概念要求类型必须是类类型(即类、结构体或联合体),但我们可以定义一个:
template<typename T>
concept Class = is_class_v<T> || is_union_v<T>; // 联合体也是类
template<typename T>
class Smart_pointer {
// ...
T& operator*() const;
T* operator->() const requires Class<T>; // -> 只有在T是类或联合体时才定义
};
通常,与直接使用标准库类型谓词相比,概念更通用或更简单适当。
16.4.3 类型生成器
许多类型函数返回类型,通常是它们计算出的新类型。我称这样的函数为类型生成器,以将它们与类型谓词区分开来。标准提供了一些,例如:
R=remove_const_t<T> | R 是去除了 T 最顶层 const (如果有的话)的类型 |
R=add_const_t<T> | R 是 const T |
R=remove_reference_t<T> | 如果 T 是引用 U& , R 是 U ;否则 R 就是 T |
R=add_lvalue_reference_t<T> | 如果 T 是左值引用,则 R 为 T ;否则为 T& |
R=add_rvalue_reference_t<T> | 如果 T 是右值引用,则 R 为 T ;否则为 T&& |
R=enable_if_t<b,T=void> | 如果 b 为真, R 是 T ;否则 R 未定义 |
R=conditional_t<b,T,U> | 如果 b 为真, R 是 T ;否则 R 是 U |
R=common_type_t<T...> | 如果存在一个类型,所有 T 都能隐式转换到该类型, R 就是那个类型;否则 R 未定义 |
R=underlying_type_t<T> | 如果 T 是枚举类型, R 是其底层类型;否则错误 |
R=invoke_result_t<T,A...> | 如果 T 能用参数列表 A... 调用, R 是其返回类型;否则错误 |
这些类型函数通常用于实现实用工具,而不是直接用于应用程序代码中。其中, enable_if 可能是概念出现之前,代码中最常见的一个。例如,智能指针的条件启用 -> 运算符的传统实现方式如下:
template<typename T>
class Smart_pointer {
// ...
T& operator*();
enable_if<is_class_v<T>,T&> operator->(); // 仅当T是一个类时,才定义->运算符
};
我发现这种写法不是特别容易读懂,而且更复杂的用法更糟糕。 enable_if 的定义依赖于一种微妙的语言特性,称为SFINAE(“替换失败不是错误”,‘Substitution Failure Is Not An Error‘)。如果你需要,可以查阅这个特性。
16.4.4 关联类型
所有标准容器(§12.8)以及所有设计为遵循它们模式的容器都有一些关联类型,如它们的值类型和迭代器类型。在 <iterator> 和 <ranges> 中,标准库为这些类型提供了名称:
range_value_t<R> | 范围 R 的元素的类型 |
iter_value_t<T> | 迭代器 T 所指向的元素的类型 |
iterator_t<R> | 范围 R 的迭代器的类型 |
16.5 source_location
在编写跟踪消息或错误消息时,我们通常希望将源代码位置作为消息的一部分。库为此提供了 source_location :
const source_location loc = source_location::current();
这里的 current() 返回一个描述源代码中出现位置的 source_location 。 source_location 类具有 file() 和 function_name() 成员,它们返回C风格的字符串,以及 line() 和 column() 成员,它们返回无符号整数。 将这个包装在一个函数中,我们就初步得到了一个日志消息的良好实现:
void log(const string& mess = "", const source_location loc = source_location::current())
{
cout << loc.file_name()
<< '(' << loc.line() << ':' << loc.column() << ") "
<< loc.function_name() << ": "
<< mess;
}
current() 的调用是一个默认参数,这样我们就可以得到 log() 的调用者的位置,而不是 log() 的位置:
void foo()
{
log("Hello"); // myfile.cpp (17,4) foo: Hello
// ...
}
int bar(const string& label)
{
log(label); // myfile.cpp (23,4) bar: <<label的值>>
// ...
}
在C++20之前编写的代码或需要在旧编译器上编译的代码使用宏 FILE 和 LINE 来实现这一点。
16.6 move() 和 forward()
移动和复制之间的选择大多是隐式的(§3.4)。当对象即将被销毁时(如在返回值中),编译器更倾向于移动,因为这被认为是更简单且更高效的操作。然而,有时我们必须明确指定。例如, unique_ptr 是某个对象的唯一所有者。因此,它不能被复制,所以如果你想要在别处使用 unique_ptr ,你必须移动它。例如:
void f1()
{
auto p = make_unique<int>(2);
auto q = p; // 错误:我们不能复制unique_ptr
auto q = move(p); // 现在p持有nullptr
// ...
}
令人困惑的是, std::move() 并不移动任何东西。相反,它将参数转换为右值引用,从而表明其参数将不再使用,因此可以移动(§6.2.2)。它本应被称为 rvalue_cast 之类的名称。它的存在是为了服务于几个关键的情况。考虑一个简单的交换:
template <typename T>
void swap(T& a, T& b)
{
T tmp {move(a)}; // T的构造函数看到一个右值并进行移动
a = move(b); // T的赋值操作看到一个右值并进行移动
b = move(tmp); // T的赋值操作看到一个右值并进行移动
}
我们不想重复复制可能很大的对象,所以我们使用 std::move() 请求移动。至于其他类型转换, std::move() 的使用虽然诱人,但也很危险。考虑:
string s1 = "Hello";
string s2 = "World";
vector<string> v;
v.push_back(s1); // 使用“const string&”参数;push_back()将进行复制
v.push_back(move(s2)); // 使用移动构造函数
v.emplace_back(s1); // 另一种方法;在v的新末尾位置放置s1的副本(§12.8)
在这里, s1 被复制(通过 push_back() ),而 s2 被移动。这有时(只是有时)会使 s2 的 push_back() 操作更便宜。问题是移动后的对象会被留下。如果我们再次使用 s2 ,就会遇到问题:
cout << s1[2]; // 输出’l’
cout << s2[2]; // 崩溃?
我认为这种使用 std::move() 的方式错误率太高,不适合广泛使用。除非你能够证明有显著且必要的性能提升,否则不要使用它。后续的维护可能会意外导致对移动后对象的非预期使用。
编译器知道返回值在函数中不会再次使用,因此使用显式的 std::move() ,例如 return std::move(x) ,是多余的,甚至可能抑制优化。
移动后对象的状态通常是不确定的,但所有标准库类型都会使移动后的对象处于可以被销毁和赋值的状态。不遵循这一点是不明智的。对于容器(例如 vector 或 string ),移动后的状态将是“空的”。对于许多类型,默认值是一个很好的空状态:有意义且易于建立。
转发参数是一个重要的用例,它需要移动(§8.4.2)。我们有时想要将一组参数原封不动地转发给另一个函数(以实现“完美转发”):
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args)
{
return unique_ptr<T>{new T{std::forward<Args>(args)...}}; // 转发每个参数
}
标准库的 forward() 与更简单的 std::move() 不同,它可以正确处理与左值和右值相关的细微差别(§6.2.2)。仅使用 std::forward() 进行转发,并且不要转发两次;一旦你已经转发了一个对象,它就不再属于你了。
16.7 位操作
在 <bit> 中,我们找到了用于低级位操作的函数。位操作是一种专门化但通常必不可少的活动。当我们接近硬件时,我们经常需要查看位,更改字节或字中的位模式,并将原始内存转换为类型化对象。例如, bit_cast 允许我们将一种类型的值转换为相同大小的另一种类型:
double val = 7.2;
auto x = bit_cast<uint64_t>(val); // 获取64位浮点数的位表示
auto y = bit_cast<uint64_t>(&val); // 获取64位指针的位表示
struct Word { std::byte b[8]; };
std::byte buffer[1024];
// ...
auto p = bit_cast<Word*>(&buffer[i]); // p指向8个字节
auto i = bit_cast<int64_t>(*p); // 将这8个字节转换为整数
标准库类型 std::byte (需要 std:: 前缀)用于表示字节,而不是已知表示字符或整数的字节。特别是, std::byte 仅提供按位逻辑操作,而不提供算术操作。通常,进行位操作的最佳类型是无符号整数或 std::byte 。这里所说的“最佳”是指最快且最不易出错。例如:
void use(unsigned int ui)
{
int x0 = bit_width(ui); // 表示ui所需的最小位数
unsigned int ui2 = rotl(ui,8); // 向左旋转8位(注意:不会改变ui)
int x1 = popcount(ui); // ui中1的个数
// ...
}
另请参阅 bitset (§15.3.2)。
16.8 退出程序
偶尔,一段代码会遇到它无法处理的问题:
• 如果这种问题频繁发生,且预期直接调用者能够处理,则返回某种返回码(§4.4)。
• 如果这种问题不常发生,或预期直接调用者无法处理,则抛出异常(§4.4)。
• 如果这种问题如此严重,以至于程序中没有任何普通部分能够处理,则退出程序。
标准库提供了处理最后一种情况(“退出程序”)的设施:
• exit(x) :调用使用 atexit() 注册的函数,然后以返回值 x 退出程序。如果需要,请查找 atexit() ,它基本上是与C语言共享的原始析构机制。
• abort() :无条件地立即退出程序,并返回一个表示终止不成功的值。某些操作系统提供了修改这一简单解释的机制。
• quick_exit(x) :调用使用 at_quick_exit() 注册的函数,然后以返回值 x 退出程序。
• terminate() :调用 terminate_handler 。默认的 terminate_handler 是 abort() 。
这些函数用于处理非常严重的错误。它们不会调用析构函数,也就是说,它们不会进行普通和适当的清理。各种处理程序用于在退出前执行操作。这些操作必须非常简单,因为调用这些退出函数的一个原因是程序状态已损坏。一种合理且相当流行的操作是“依赖当前程序之外的任何状态,在明确定义的状态下重启系统。”另一种稍显棘手,但往往并非不合理的操作是“记录错误消息并退出。”编写日志消息可能是一个问题的原因是,导致调用退出函数的任何原因都可能已损坏I/O系统。
错误处理是最棘手的编程类型之一;即使干净地退出程序也可能很困难。
通用库不应无条件终止。
16.9 建议
- 库不一定非得很大或很复杂才有用;§16.1。
- 在声明效率之前,先对你的程序进行计时;§16.2.1。
- 使用 duration_cast 以适当的单位报告时间测量;§16.2.1。
- 要在源代码中直接表示日期,请使用符号表示法(例如, November/28/2021 );§16.2.2。
- 如果日期是计算的结果,请使用 ok() 检查其有效性;§16.2.2。
- 在处理不同位置的时间时,请使用 zoned_time ;§16.2.3。
- 使用 lambda 表达式来表示调用约定中的微小变化;§16.3.1。
- 使用 mem_fn() 或 lambda 表达式创建函数对象,以便在使用传统函数调用表示法调用时能够调用成员函数;§16.3.1,§16.3.2。
- 当你需要存储可以调用的内容时,请使用函数;§16.3.3。
- 优先考虑使用概念,而不是显式使用类型谓词;§16.4.1。
- 你可以编写代码,以显式依赖于类型的属性;§16.4.1,§16.4.2。
- 只要可能,优先考虑使用概念而不是特性和 enable_if ;§16.4.3。
- 使用 source_location 在调试和日志消息中嵌入源代码位置;§16.5。
- 避免显式使用 std::move() ;§16.6;[CG: ES.56]。
- 仅使用 std::forward() 进行转发;§16.6。
- 在对对象执行 std::move() 或 std::forward() 操作后,切勿再从中读取;§16.6。
- 使用 std::byte 表示尚不具有有意义类型的数据;§16.7。
- 使用 unsigned int 或 bitset 进行位操作;§16.7。
- 如果预期直接调用者能够处理问题,则从函数返回一个错误码;§16.8。
- 如果预期直接调用者无法处理问题,则从函数抛出一个异常;§16.8。
- 如果尝试从问题中恢复是不合理的,则调用 exit() 、 quick_exit() 或 terminate() 来退出程序;§16.8。
- 通用库不应无条件地终止;§16.8。