C++ 核心指南(C++ Core Guidelines)是由 Bjarne Stroustrup、Herb Sutter 等顶尖 C++ 专家创建的一份 C++ 指南、规则及最佳实践。旨在帮助大家正确、高效地使用“现代 C++”。

这份指南侧重于接口、资源管理、内存管理、并发等 High-level 主题。遵循这些规则可以最大程度地保证静态类型安全,避免资源泄露及常见的错误,使得程序运行得更快更好。

R. Resource Management

本篇的基本目标是:不产生资源泄漏,不持有不再需要的资源。资源是指需要申请和(显式或隐式)释放的任何东西,如内存、文件句柄、套接字、锁等。如果一个实体“拥有”资源,则意味着该实体负责释放资源。在个别场景下,资源泄露是可以接受的。例如有足够的内存处理最大的输入,或者出于性能优化考虑,只申请不释放,本章的规则不适用于这类特殊情况。

  • 资源管理规则总结

    • R.1: 通过资源句柄和 RAII 自动管理资源
    • R.2: 在接口中,裸指针只用来表示单个对象
    • R.3: 裸指针(T*)不拥有资源
    • R.4: 裸引用(T&)不拥有资源
    • R.5: 优先使用对象,非必要不在堆上分配
    • R.6: 避免使用非 const 的全局变量

R.1: 通过资源句柄和 RAII 自动管理资源

避免泄露以及手动管理资源的复杂性。C++ 的构造/析构反映了资源获取/释放的固有对称性,如 fopen/fclose,lock/unlock,new/delete。在处理资源时,如果需要通过一对函数获取/释放资源,则可以把该资源封装到一个类中:在构造中获取资源,析构中释放资源。

反面例子

void send(X* x, string_view destination)
{
auto port = open_port(destination);
my_mutex.lock();
// ...
send(port, x);
// ...
my_mutex.unlock();
close_port(port);
delete x;
}

在这个例子中,必须记得在所有路径上调用 unlock、close_port、delete,并且要保证刚好调用一次,并且一旦 ... 部分抛出异常,就会导致资源 x 泄露,my_mutex 也一直是 locked 状态。

正面例子

void send(unique_ptr<X> x, string_view destination)
{
Port port{destination}; // port 拥有 PortHandle
lock_guard<mutex> guard{my_mutex}; // guard 拥有锁
// ...
send(port, x);
// ...
} // 自动 unlock my_mutex 并且 delete x

现在所有资源都是自动释放的,并且保证在所有路径上,无论是否有异常抛出,都只释放一次。不仅如此,这个函数也明确宣布了它对指针 x 的所有权。

Port 只是一个简单的 wrapper,封装了资源:

class Port {
PortHandle port;
public:
Port(string_view destination) : port{open_port(destination)} { }
~Port() { close_port(port); }
operator PortHandle() { return port; } // port 句柄通常不能复制,如果有必要,禁用拷贝构造和拷贝赋值运算符
Port(const Port&) = delete;
Port& operator=(const Port&) = delete;
};

如果资源不是以“具有析构函数的类”的方式呈现,就用类封装一下,或者使用 GSL 库中的 finally ,以便资源能够自动释放

相关惯用法

RAII

R.2: 在接口中,裸指针只用来表示单个对象

换句话说,不要用裸指针表示一个数组。数组最好用容器类型(如:拥有资源的用 vector、不拥有资源的用 span),容器、视图包含大小信息,可以进行边界检查。

反面例子

void f(int* p, int n)   // n 是 p[] 元素的数量
{
// ...
p[2] = 7; // bad: 裸指针下标
// ...
}

编译器又不看注释,如果不看其他代码,你无法确定 p 是否真的指向 n 个元素。这种情况最好用 span

例子

void g(int* p, int fmt)   // 用 #fmt 格式打印 *p
{
// ... 仅使用 *p 和 p[0] ...
}

例外

C 风格字符串通过单个指针(指向以 \0 结尾的字符序列)传递,如果你依赖这种约定,最好使用 zstring,而不是 char*

很多指向单一元素的指针都可以用引用替代,除非指针可能是 nullptr

代码检查建议

  • 如果一个指针不是容器、视图或迭代器,且对该指针进行了算术运算(包括 ++),则标记该指针上的算术运算。如果将该规则应用在旧的代码上,可能产生大量误报
  • 标记作为简单指针传递的数组

R.3: 裸指针(T*)不拥有资源

在绝大多数的代码中,裸指针都是“非拥有”(non-owning)的,即指针不拥有资源,使用该指针的代码不负责释放指针指向的资源。

我们希望能够标识出“拥有指针”(owning pointer),这样就能安全地释放“拥有指针”指向的资源。

例子

void f()
{
int* p1 = new int{7}; // bad: 裸“拥有指针”,f() 要负责释放 p1 指向的资源
auto p2 = make_unique<int>(7); // OK: unique_ptr 拥有 int
// ...
}

unique_ptr 可以保证及时释放资源,即使发生异常也不会产生泄漏,但是 T* 无法保证。

例子

template<typename T>
class X {
public:
T* p; // bad: 不清楚 p 是否是“拥有指针”
T* q; // bad: 不清楚 q 是否是“拥有指针”
// ...
};

上面的代码无法知道 p、q 是否是“拥有指针”,可以通过下面的方式明确所有权:

template<typename T>
class X2 {
public:
owner<T*> p; // OK: p 是“拥有指针”
T* q; // OK: q 不是“拥有指针”
// ...
};

owner<T*> 本质上也是 T*,用 owner<T*> 替代 T* 不会影响 ABI 及原来的代码,它只是用来提示程序员和代码分析工具。如果 owner<T*> 是类的成员,通常应该在类的析构中释放指向的资源。

例外

主要是历史遗留代码,尤其是那些要需要和 C 或者和 C 风格 C++ ABI 兼容。我们不能把所有的“拥有指针”转换成 unique_ptr 或 shared_ptr 来解决这个问题,部分原因是我们在基本的资源管理代码中需要/使用了裸“拥有指针”。例如常见的 vector 实现中,有一个“拥有指针”和两个“非拥有指针”。许多 ABI(以及所有 C 代码接口)都还使用 T*,其中有些是“拥有指针”。有些接口不能简单地用 owner<T*> 标注,因为要和 C 兼容(这个问题可以用宏来解决,只在 C++ 时展开宏,这是宏少有的正确使用场景)。

反面例子

返回裸指针使得调用者无法确切知道该如何进行生命周期管理:调用者是否需要释放指针指向的对象?

Gadget* make_gadget(int n)
{
auto p = new Gadget{n};
// ...
return p;
} void caller(int n)
{
auto p = make_gadget(n); // 调用者要记得删除 p
// ...
delete p;
}

上述代码除了泄漏问题之外,还可能增加不必要的分配/释放操作。如果 Gadget 很小或者移动成本很低,可以直接按值返回(详见 R.5):

Gadget make_gadget(int n)
{
Gadget g{n};
// ...
return g;
}

本规则适用于工厂方法

在必须使用指针的情况下(比如涉及到多态,返回基类指针),返回智能指针

代码检查建议

  • 如果 delete 了一个不是 owner<T> 的裸指针,给出警告
  • 如果没有在所有路径上 reset 或 delete 某个 owner<T>,给出警告
  • 如果 new 返回的结果赋给一个裸指针,给出警告
  • 如果函数返回了一个在函数内分配的对象,且该对象有移动构造,给出警告,建议考虑按值返回

R.4: 裸引用(T&)不拥有资源

和 R.3 一样,大多数的裸引用也不拥有资源。

例子

void f()
{
int& r = *new int{7}; // bad: 裸“拥有引用”
// ...
delete &r; // bad: 删除裸指针,违反 R.3 规则
}

R.5: 优先使用作用域对象,非必要不在堆上分配

作用域对象(scoped object)可以是局部对象、全局对象或者类成员。除了作用域对象本身,没有额外的分配/释放的开销。作用域对象内部的成员的生命周期由作用域对象的构造/析构管理。

例子

下面代码存在的问题:1. 不必要的分配/释放;2. 如果 ... 部分抛异常,会导致内存泄漏;3. 代码冗长

void f(int n)
{
auto p = new Gadget{n};
// ...
delete p;
}

改用局部对象则可解决上述 3 个问题:

void f(int n)
{
Gadget g{n};
// ...
}

代码检查建议

  • 如果一个函数内,某个对象在所有路径上都是先分配,再释放,给出警告。建议用局部栈对象。
  • 如果一个局部非 const 的 unique_ptr 或者 shared_ptr 在其生命周期结束之前没有被 move、拷贝、重新赋值或者 reset,给出警告
    • 例外:如果是一个指向动态数组的局部 unique_ptr,不给出警告,见下面例子:

例外

可以创建一个堆上分配缓冲区的 const unique_ptr<T[]>,这是表示动态数组的合法方式

例子

int get_median_value(const std::list<int>& integers)
{
const auto size = integers.size(); // OK: 声明一个局部 unique_ptr<T[]>.
const auto local_buffer = std::make_unique_for_overwrite<int[]>(size); std::copy_n(begin(integers), size, local_buffer.get());
std::nth_element(local_buffer.get(), local_buffer.get() + size/2, local_buffer.get() + size); return local_buffer[size/2];
}

R.6: 避免使用非 const 的全局变量

(同规则 I.2)非 const 的全局变量隐藏了依赖,容易受到无法预测的改变。

例子

struct Data {
// ... lots of stuff ...
} data; // non-const data void compute() // don't
{
// ... use data ...
} void output() // don't
{
// ... use data ...
}

你很难知道是否还会有其他地方改变 data

警告

全局对象的初始化顺序不能完全确定,所以只能用常量去初始化全局对象。另外,即使是 const 对象,其初始化顺序也可能是未定义的。

例外

即便是全局对象,一般来说也比单例好

  • 全局常量是有用的

  • 本规则除了适用于“全局变量”外,对命名空间里的变量也适用

替代方案

如果是出于避免拷贝的目的而使用全局变量,可以考虑用 reference to const 来传递数据。或者把该数据作为对象的状态(即类的成员),然后通过成员函数是来操作该成员。

警告

小心数据争用(data race):如果一个线程可以访问 non-local 数据(或者引用传递的数据),同时另一个线程执行 callee,就可能产生 data race。每个指向可变数据的指针或引用都可能产生数据争用

不可变数据(immutable data)不会产生静态条件(race condition)

这条规则是“尽量避免”,不是“禁止使用”。会有一些(极少数)例外,比如 cincoutcerr

代码检查建议

报告所有在命名空间/全局中声明的非 const 变量及指向非 const 对象的指针或引用

C++ 核心指南之资源管理(上)的更多相关文章

  1. Robot Framework自动化测试框架核心指南-如何做好自动化测试平台框架的设计

    自动化测试如果需要能高效快速的支撑软件项目的测试,项目的快速迭代以及上线,除了以上我们介绍的需要许多的Lib来支持以及需要高效的去编写自动化测试案例外,还需要一个好的自动化测试框架平台来支撑我们的自动 ...

  2. 三年磨一剑,robot framework 自动化测试框架核心指南,真正讲透robot framework自动化测试框架(笔者新书上架)。

    序 关于自动化测试的工具和框架其实有很多.自动化测试在测试IT行业中扮演着越来越重要的角色,不管是在传统的IT行业还是高速发展的互联网行业或是如今的大数据和大热的人工智能领域,都离不开测试,也更加离不 ...

  3. Robot Framework自动化测试框架核心指南-如何使用Java编写自定义的RobotFramework Lib

    如何使用Java编写自定义的RobotFramework Lib 本文包括2个章节 1. Robot Frdamwork中如何调用java Lib库 2.使用 java编写自定义的Lib 本文作者为: ...

  4. 【Windows】windows核心编程整理(上)

    小续 这是我11年看<windows核心编程>时所作的一些笔记,现整理出来共享给大家 windows核心编程整理(上) windows核心编程整理(下) 线程的基础知识 进程是不活泼的,进 ...

  5. 《Velocity java开发指南》中文版(上)转载

    文章引自:http://sakyone.iteye.com/blog/524289 1.开始入门 Velocity是一基于java语言的模板引擎,使用这个简单.功能强大的开发工具,可以很容易的将数据对 ...

  6. 高性能消息队列 CKafka 核心原理介绍(上)

    欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 作者:闫燕飞 1.背景 Ckafka是基础架构部开发的高性能.高可用消息中间件,其主要用于消息传输.网站活动追踪.运营监控.日志聚合.流式 ...

  7. [译] JavaScript核心指南(JavaScript Core) 【转】

    本文转自:http://remember2015.info/blog/?p=141#scope-chain 零.索引 对象(An Object) 原型链(A Prototype Chain) 构造函数 ...

  8. JS核心之DOM操作 上

    JS一个重要功能就是操作DOM, 改变页面显示. 目录: 1.基本概念 2.节点类型 3.节点关系 4.节点操作 基本概念 DOM全称为Document Object Model ,即文档对象模型,是 ...

  9. Mybatis-Plus 实战完整学习笔记(九)------条件构造器核心用法大全(上)

    一.Mybatisplus通用(公共方法)CRUD,一共17种(3.0.3版),2.3系列也是这么多,这个新版本一定程度进行了改造和删减. 二.构造器UML图(3.0.3)-----实体包装器,主要用 ...

  10. PMP:9.项目资源管理

    项目资源管理包括识别.获取和管理所需资源以成功完成项目的各个过程,这些过程有助于确保项目经理和项目团队在正确的时间和地点使用正确的资源. 项目资源管理过程包括:   团队资源管理相对于实物资源管理,对 ...

随机推荐

  1. 【开源项目】合肥~超经典智慧城市CIM/BIM数字孪生可视化项目—开源工程及源码

    最新消息,数字孪生智慧宁波开源了其数据工程源码和工程,免费送出供大家学习.使用.分享. ​ 智慧宁波实现了一系列全面的功能,如实现长三角经济圈特效.智慧地铁特效.智慧灯杆特性等.这些项目利用数字孪生技 ...

  2. [Linux]CentOS7搭建/配置:YUM仓库/源[本地源/Web源(Apache HTTP(D))/自建源仓库]

    若想搞懂整个配置过程和原理,就按照章节(1 / 2)一步一步地来. 若想直接一步到位,不想花过多时间,尽快配好,就直接看附件章节. 什么是yum源? Yum(全称为 Yellow dog Update ...

  3. Rust中的into函数和from函数

    1.Rust中的into函数和from函数是做什么用的? into函数是Rust语言中的一个转换函数,它属于Into trait.它可以将一个类型转换为另一个类型.实现了From trait的类型会自 ...

  4. DG:switchover切换操作

    问题描述:我们配置DG的目的就是为了在主库出现故障时,备库能够提供服务,保证业务的正常运行,switchover是用户有计划的进行停机切换,能够保证不丢失数据,我记录一下我进行switchover中的 ...

  5. Go语言核心知识回顾(反射)

    有时要求写一个函数有能力统一处理各种值类型的函数,而这些类型可能无法共享同一个接口,也可能布局未知,也有可能这个类型在设计函数时并不存在,当我们无法透视一个未知类型的布局时,这段代码就无法继续,这是就 ...

  6. 【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II)

    零钱兑换II 力扣题目链接(opens new window) 给定不同面额的硬币和一个总金额.写出函数来计算可以凑成总金额的硬币组合数.假设每一种面额的硬币有无限个. 示例 1: 输入: amoun ...

  7. js计算当前时间差

    1 function timesFun(timesData) { 2 //如果时间格式是正确的,那下面这一步转化时间格式就可以不用了 3 var dateBegin = new Date(timesD ...

  8. [C++核心编程] 2、引用

    文章目录 2 引用 2.1 引用的基本使用 2.2 引用注意事项 2.3 引用做函数参数 2.4 引用做函数返回值 2.5 引用的本质 2.6 常量引用 2 引用 2.1 引用的基本使用 **作用: ...

  9. PaddlePaddle 自动求导

    自动求导 在 PaddlePaddle 中使用自动求导来计算导数. 要求:$ f(x)=\sin{x} $,绘制 \(f(x)\) 和 \(\dfrac{\mathrm{d}f(x)}{\mathrm ...

  10. Nginx Web快速入门

    Nginx Web快速入门 目录 Nginx Web快速入门 Nginx概述 为什么选择Nginx服务 Nginx的应用场景 源码安装nginx Yum安装nginx Nginx相关命令总结 Ngin ...