C++ 踩坑:特殊成员函数 - The Rule of Zero

507 阅读5分钟

特殊成员函数 - The Rule of Zero

上篇文章 - "The Rule of The Big Four (and a half)", 我们分析C++中的资源管理策略.

资源管理是 C++ 中一个普遍的话题, 确保资源 - 文件, 动态内存, sockets, 互斥量等 - 具备自动受控的生命周期, 以避免资源的泄露, 思索等. C++ 将这些机制称为 RAII/RDID.

在这篇文章中, 我们将来了解一种互补的指导准则来帮助简化你们的应用代码, 避免资源管理的风险 -The Rule of Zero.

术语 The Rule of Zero 是 R. Martinho Fernandes 在他的2012 年的一篇论文中提出的(flamingdangerzone.com/cxx11/2012/…). 本文仅仅反映了 Martinho Fernandes 的工作. 我强烈建议阅读原文来了解这些概念的细节.

如果你对资源管理的概念不是很熟悉, 在阅读本文之前, 我建议你最好看一下之前的文章 - The Rule of Three (and a half) 和 The Rule of Four (and a half).

资源管理的四种类型

从资源管理的视角出发, 我们可以将其分为四种类型:

  • 对象可以被移动和拷贝.
  • 对象仅能被拷贝, 但不能被移动.
  • 对象仅能被移动, 但不能被拷贝.
  • 对象既不能被移动, 也不能被拷贝.

The Rule of Four (and a half) 是实现类型移动/拷贝策略的准则. 本质上, 陈述如下:

  • 如果你为类编写了(非默认) 析构函数, 你应该也为该类实现拷贝构造函数和拷贝赋值操作符, 或者将它们标记为删除.
  • 同理, 如果你为类编写了 (非默认) 拷贝构造函数和拷贝赋值操作符, 你必须编写一个析构函数.
  • 如果你的类型可以被移动, 你必须同时实现移动构造和移动赋值操作符, 或者将它们标记为删除.

基于拷贝策略, 经常会有内存, 速度, 效率等性能问题, 取决于拷贝一个类是否"合理". 举个例子, 拷贝 1MB 资源数据可能不是很明智.

在其他案例中, 结果可能取决于, 复制一个已有所属的资源是否可能会造成灾难性的后果. 举个例子, 复制一个系统互斥量可能会造成潜在的, 难以定位的竞争状态.

移动资源通常作为对拷贝的有效优化方案. 然而, 被移动的对象必须置于"空"状态. 如何"定义"空(显然)不同对象是不一样的. 但是一个好的规则是: 如果对象没有默认的构造函数, 那么它就不应该支持移动语义.

The Rule of Zero

"The Rule of The Big four (and a half)" 的一个备选方法则以 "The Rule of Zero" 的形式出现. "The Rule of Zero" 基本陈述如下:

你不应该在你的代码里实现析构函数, 拷贝构造函数, 移动构造函数和赋值操作符.

一个(非常重要)的推论:

你不应该通过原始指针来管理资源.

The Rule of Zere 的目的是为了将资源管理交给标准库来构建, 让它们帮你完成所有的困难工作, 从而来简化你的应用程序代码.

The Rule of Zero 和动态内存

如果你的代码必须动态创建对象, 建议使用 std::unique_ptr 或者 std::shared_ptr. 如果你的类可以被移动, 但是不允许拷贝, 那么使用 std::unique_ptr:

如果你需要既能拷贝又能移动, 那么使用 shared_ptr:

因为编译器会为你的类生成默认的拷贝构造函数, 移动构造函数和赋值操作符, 所以代码可以正常工作. 这些默认实现将会为 shared_ptr/ unique_ptr (如果可行) 调用合适的函数. C++ 标准会限制这些构造函数的创建, 如下:

Section: 12.8/8:

如果类 X 的定义没有显式声明移动构造函数, 那么将会隐式声明一个默认版本的, 当且仅当

  • X 没有用户声明的构造函数, 并且
  • X 没有用户声明的拷贝赋值操作符,
  • X 没有用户声明的移动赋值操作符,
  • X 没有用户声明的析构函数, 并且 移动构造函数不会被隐式定义为删除.

(Section 12.8/22 也为赋值操作符阐述了一个非常类似的规则)

换句话说, 就是 "The Rule of Zero".

The Rule of Zero 和字符串

在字符串的案例中, The Rule of Zero 更偏向使用 std::string 来替代字符串数组 - 特别是动态分配的数组 (指变长数组).

注意 std::string 支持拷贝和移动语义. 在移动之后, 字符串对象将置"空" (这个例子中, 为空字符串).

The Rule of Zero 和容器

数组是 C++ 唯一内置容器. 有时会把它视作 "退化" 的容器, 因为这仅仅指针算法的语法糖.

数组符号隐藏了一个事实(问题所在!), 数组并没有比原始指针好多少, 使得它很可能有意无意地被滥用. 因此, The Rule of Zero 的推论数组依然有效: 不要使用数组. 尤其是对于那些被动态创建(new)出来的数组.

反之, The Rule of Zero 建议使用标准库的容器类型.

如果不需要变长容器, 建议使用 std::array 来替代内置数组. 当 std::array 被移动时, 它会为其中的每个元素调用移动赋值操作符(和其他所有对象一样, 如果数组中的元素不支持移动, 那么元素将被拷贝).

总结

The Rule of Zero 是简化应用程序代码, 同时也是避免C++ 编程中资源管理相关问题的准则.

The Rule of Zero 是编写 C++ "现代" 的方法. 几乎没有有理由再返回去使用 C 风格方式手动管理内存. 经验和证据已经表明, 作为开发者的我们并不是很擅长使用它.

参考资料

The Rule of Zero - Sticky Bits - Powered by FeabhasSticky Bits – Powered by Feabhas

The rule of three/five/zero - cppreference.com

Tutorial: When to Write Which Special Member (foonathan.net)