CppCoreGuidelines 二周目笔记 00
原话: One way of thinking about these guidelines is as a specification for tools that happens to be readable by humans.
我的观点是不要死记硬背,最简单的方法是把 clang-tidy 之类的工具集成到你的开发环境里
重读一遍 Cpp Core Guidlines ,记一些我喜欢的条目,不保证我的理解百分百正确。
00. 对复制操作廉价的输入参数使用值类型传递(F.16)
1 | void foo(int i); // 没毛病,复制开销可忽略不计,甚至还可能被优化成移动 |
什么情况要用右值:
- 函数需要保留参数的一个副本然后传给另一个目标(其他函数或存储到非局部位置),在
const T&基础上加一个T&&重载,然后移动给另一个目标 - 函数仅需要自己局部使用,使用
T - 函数需要无条件从参数进行移动,使用
T&&
不要为了「神奇的优化」到处加 T&&。
空引用: C++ 没有这种东西,引用始终表示某个有效的对象,可以考虑用智能指针、
std::optional或代表「没有值」的特殊值(大概像std::string::npos?)
01. 转发的使用情况(F.19)
函数中不直接使用的参数可以 std::forward 给目标,目的是不影响参数的 const 性质和右值性质:
1 | template<typename F, typename... Args> |
02. 不要返回 std::move(local_variable) (F.48)
这样写就足够了:
1 | Type foo() { |
不要瞎操心:
1 | Type foo() { |
返回 std::move(local_variable) 一般都是瞎操心,因为 RVO 最好的情况可以完全消除移动构造,最差的情况和 std::move(local_variable) 相同
03. 慎重在 lambda 里用引用捕获局部变量(F.53)
一般来说用引用捕获是最有效率的,但如果这个 lambda 并不一定在与局部变量相同的作用域里使用(比如是要被返回的、要传给另一条线程的、在堆上动态分配的),慎重用引用捕获来捕获局部变量,比如:
1 | { // 某个作用域里 |
- 如果必须捕获这种对象的引用,可以把这种对象用
std::unique_ptr包装,然后捕获std::unique_ptr来自动处理生命周期问题。 - 如果要捕获
this,可以采取*this,这样会创建一个副本。
04. 构造函数应该完整构造对象(C.41)
就是说对象被构造之后应该就是可用的,像这样的设计是错的:
1 | Type v; |
如果无论如何都需要构造函数以外的操作才能完整地初始化一个对象(比如需要调用虚函数),考虑工厂模式:
1 | class Factory { |
( BS/HS 推荐了 std::unique_ptr<Type> 作返回值而不是 Type ,但我个人认为如果 Type 有移动构造而且很廉价,用 Type 作返回值也可以吧)
05. 可复制类应该有默认构造函数(C.43)
为了这样的语句是有效的:
1 | std::vector<Type> v(100); // 含有 100 个默认构造的 Type 对象 |
如果没有默认构造函数,上面的语句不能编译通过,需要像这样来 work around :
1 | std::vector<Type> v(100, Type{364, 364}); |
06. 不对自赋值进行检测(C.62, C.65)
1 | Type& Type::operator=(const Type& v) { |
这样确实是安全的,但如果一亿次赋值中只有一次自赋值,则会浪费近一亿次比较 this == &v ,会对效率造成一定影响,如果 foo 和 bar 都能保证其自赋值不会产生恶性影响,那么建议忽略自赋值的检测:
1 | Type& Type::operator=(const Type& v) { |
07. 多态类避免公开的复制构造(C.67, C.130, C.145)
1 | class Base { |
这里 foo 里的 b1 只能拿到 Base 里的数据,虽然传进去的是 Derived 类型,但里面的 b1 只能复制成 Base 类型,所以最好把复制构造放进 private/protected 或 = delete 。如果实在需要复制,可以定义 clone 函数:
1 | class Base { |
这样会导致这样的代码是错误的:
1 | void foo(Base b) { |
访问时用指针或引用:
1 | void foo(Base& b) { |
08. 为值类型提供 noexcept 且不会失败的 swap 函数(C.83, C.84, C.85)
1 | class Type { |
为了调用者方便,还可以在同一个命名空间定义一个非成员的 swap :
1 | void swap(Type& l, Type& r) { |
标准库里的容器和算法有很多用到 swap 的地方,如果不能保证 swap 不会失败(且 noexcept ),标准库的相关工具也无法正常工作,比如这种写法是设计上失败的:
1 | void swap(Type& l, Type& r) { |
如果创建 temp 涉及到了堆内存分配,就可能抛出异常,而且这种写法也是低效的,有种 Java 的美。
09. 比较运算符两边操作数应该对称且应该是 noexcept 的(C.86, C87)
!=, <, <=, >, >= 这些运算符应该是 noexcept 的,而且两边的操作数应该一致,比如这样:
1 | struct Data { |
但如果是这样:
1 | struct Data { |
两边操作数不对称,左边必须是 Data 类型,而右边可以类型转换,比如:
1 | class Base { |
TL;DR 这些比较运算符不应该是成员函数,也不应该是 virtual 的。
10. 如果非要自己特化 hash 就做成 noexcept 的(C.89)
在 cppreference 的一个例子上改的:
1 | struct S { |
11. 避免 protected 成员变量(C.133)
优先用 private , protected 成员函数没有问题。
我属于是跟 Gtkmm 学坏的,即使 CppCoreGuidelines 这么说了,我觉得 Gtkmm 用
protected也是合理的,我用 Gtkmm 时也会用protected(大概)
12. 优先用 virtual 函数再考虑 dynamic_cast (C.146, C.147)
首先 dynamic_cast 保证了安全,是应该使用的,但同时可能会对性能造成影响,如果可能的话用 virtual 函数,比如:
1 | struct Base { |
这样写就是低效的:
1 | void foo(Base& b) { |
这里对指针进行
dynamic_cast如果失败,结果会是nullptr,不会抛出异常,所以如果类型转换失败是一种有效的可能的情况,可以对指针使用dynamic_cast
不如直接:
1 | void foo(Base& b) { |
或者如果已经通过一些检查确定了对象的具体类型,且十分在意 dynamic_cast 带来的性能损耗( BS 的意思是目前 dynamic_cast 的实现是很快的),可以优先使用 static_cast :
1 | void foo(Base& b) { |
但 static_cast 这样用是导致漏洞的一大原因,见仁见智吧。
如果类型转换失败是一种错误,可以对引用使用 dynamic_cast ,失败会抛出 bad_cast :
1 | void foo(Base& b) { |
13. 实参依赖查找 (ADL)
比如:
1 | int main() { |
里面的 << 「operator<<(std::ostream&, const char*)」 并没有在当前的命名空间,而是在 std 命名空间里,所以对编译器比较友善的写法是这样的:
1 | std::operator<<(std::cout, "hello world!\n"); |
而实际上第一种写法是可以通过编译的,这就是靠 ADL 实现的,因为 operator<< 的左实参 std::cout 指定了其在 std 命名空间里, ADL 就可以顺便找到 std::operator<<(std::ostream&, const char*) ,但 ADL 只能找函数调用表达式中的函数名, i.e. 这样的写法是错误的:
1 | std::cout << endl; |
ADL 只会帮你找到 << ,而不会帮 << 找到它的右实参 endl 。
还有一些奇怪的写法:
1 | endl(std::cout); // 正确的 |
14. 枚举不要全大写(Enum.5)
会与宏产生冲突,比如:
1 |
|
我觉得不好评价
15. 不要无理由指定枚举底层类型(Enum.7)
我经常这样写:
1 enum class Enum : std::uint8_t { FOO, BAR, BAF };
不指定底层类型默认采用 int ,这样是与 C 的 enum 兼容的,读写都是最简单的。
16. shared_ptr, unique_ptr 和 weak_ptr 的使用情景(R.20..24,32..37)
还在 new, delete 的都是原始人,现在都流行用 unique_ptr, shared_ptr 表示所有权了,什么?你说你还在:
1 | std::unique_ptr<Data> data {new Data{114514}}; |
原始人!现在都流行:
1 | std::unique_ptr<Data> data = std::make_unique<Data>{114514}; |
优先用 unique_ptr ,需要共享所有权时再用 shared_ptr (比如局部范围内使用)。
作参数时
unique_ptr<Type>表示所有权的转移unique_ptr<Type>&表示对资源的重新分配(修改这个指针的目标,如赋值或reset()),如果单纯是对里面的对象进行修改,可以改用Type&const unique_ptr<Type>&烂活,改用const Type&shared_ptr<Type>表示共享所有权shared_ptr<Type>&表示对资源的重新分配const shared_ptr<Type>&表示后面会把这个参数复制/移动到别的地方,其他任何情况可以改用Type&类型参数shared_ptr<Type>&&烂活,不如直接传值shared_ptr<Type>