现代C++应用之Rule of Zero

Posted 软件工程师之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了现代C++应用之Rule of Zero相关的知识,希望对你有一定的参考价值。

Kate Gregory曾在Meeting C++ 2017上发表主题演讲:It's Complicated,向与会者展示了C++语言简洁实现背后的复杂之处(强烈推荐).

C++语言的确很复杂,开发者必须了解很多才能写出恰当的实现,但是,这并不代表恰当的实现只能很复杂. 目前C++社区维护了开源项目 C++ Core Guidelines,来向开发者传播 现代C++ 的最佳实践.

本系列文章旨在从应用角度,阐述如何使用现代C++,如有错漏,欢迎指正.

什么是Rule of Zero

Rule of Zero是Peter Sommerlad在 Simpler C++ With C++11/14 中创造的术语:

Write your classes in a way that you do not need to declare/define neither a destructor, nor a copy/move constructor or copy/move assignment operator

Use smart pointers & standard library classes for managing resources

即:借助于智能指针以及标准库中提供的类型来管理资源,利用编译器默认生成的实现,开发者定义class不需要声明或定义析构、拷贝构造、移动构造、拷贝赋值、移动赋值等系列函数.

针对类声明或定义,随着标准的变化,发展顺序是:

  1. Rule of Three

    该规则指出,如果一个class定义了构造函数,则其几乎总是要定义拷贝构造函数和拷贝赋值操作,事实上它是两条规则:

    1. 如果你定义了构造函数,你可能需要定义拷贝构造函数和赋值操作

    2. 如果你定义了拷贝构造函数或者赋值操作,那么你可能两个都需要,并且也需要实现析构函数

  2. Rule of Five

    相比Rule of Three多了移动构造、移动赋值两个函数,规则较为复杂,文章后续讲解.

  3. Rule of Zero

为什么开发者要绕过编译器提供自己的实现?通常有两种情况:

  1. 管理资源
  2. 多态的析构及虚函数

基于Rule of Zero,这两种情况该如何处理呢?

管理资源

C++98/03中,如果需要与一些第三方库交互,可能会面临资源管理的问题,这时开发者需要遵循Rule of Three:

struct example_t
{

example_t():m_ptr(API::createResource()){};
~example_t() { API::releaseResource(m_ptr);}
private:
example_t(example_t const&);
example_t& operator=(example_t const&);
API::Resource* m_ptr;
};

而在C++11/14中,则需要遵循Rule of Five:

struct example_t
{

example_t():m_ptr(API::createResource()){};
~example_t() { API::releaseResource(m_ptr);}

example_t(example_t const&) = delete;
example_t& operator=(example_t const&) = delete;

example_t(example_t && other):m_ptr(other.m_ptr){ other.m_ptr = nullptr;} ;
example_t& operator=(example_t && other){
example_t tmp {std::move(other)};
std::swap(m_ptr,tmp.m_ptr);
return *this;
}
private:
API::Resource* m_ptr;
};

应用Rule of Zero,写法为:

struct example_t
{

example_t():m_ptr(API::createResource(),&API::ReleaseResourceWrap){};
private:
std::unique_ptr<API::Resource,decltype(&API::ReleaseResourceWrap)> m_ptr;
};

由于使用了智能指针std::unique_ptr,编译器会自动删除拷贝构造、拷贝赋值函数,自动生成合适的析构、移动构造、移动赋值函数.

运行时多态

当开发者使用运行时多态时,被告知如果类中声明有虚函数,则必须要将析构函数声明成为虚方法,否则无法正确析构,例如:

struct itask_t
{

virtual void run() = 0;
};

要写成:

struct itask_t
{

virtual ~itask_t()=default;
virtual void run() = 0;
};

Simpler C++ With C++11/14 的第二部分Use smart pointers & standard library classes for managing resources中给出了解决方案:

Under current practice, the reason for the virtual destructor is to free resources via a pointer to base. Under the Rule of Zero we shouldn’t really be managing our own resources, including instances of our classes.

即:

struct task_t{
virtual void run() = 0;
};

struct task_impl :public task_t
{
void run() override;
};

void apply(){
std::shared_ptr<task_t> task = std::make_shared<task_impl>();
//脱离作用域则task析构
//task自身是std::shared_ptr<task_impl>的副本,能够正确找到析构函数
}

关于构造函数

在一些类声明中,构造函数的目标只是为了初始化成员变量,C++11标准允许为成员变量提供默认初始化操作,并且提供了通用初始化语法,如果构造函数的目的单纯为了把成员变量赋初值,可以参考如下写法:

class person_t
{

private:
std::string name = "empty";
unsigned char age = 20;
bool sex = true;
};

通过这种方式,构造函数也非必须的.

关于标准约束

C++03中,并没有强制约束Rule of Three,编译器会选择提供默认实现,这样哪怕用户代码存在问题,也不会警示开发者:

§ 12.4 / 3 If a class has no user-declared destructor, a destructor is declared implicitly

§ 12.8 / 4 If the class definition does not explicitly declare a copy constructor, one is declared implicitly.

§ 12.8 / 10 If the class definition does not explicitly declare a copy assignment operator, one is declared implicitly

因为存在问题,C++11中废弃了之前的标准行为:

D.3 Implicit declaration of copy functions [depr.impldec]

The implicit definition of a copy constructor as defaulted is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor. The implicit definition of a copy assignment operator as defaulted is deprecated if the class has a user-declared copy constructor or a user-declared destructor. In a future revision of this International Standard, these implicit definitions could become deleted

也就是说,符合C++11标准的编译器同样会生成默认的实现,但是编译器至少需要产生警告来提醒用户(知道警告的重要性了吧),虽然一直有讨论说要禁止而不是废弃这种行为,但是C++14标准和C++17标准并没有全面禁止掉该行为(后向兼容需要付出的代价).

之前Rule of Three要求实现三个方法,Rule of Five在其基础上新增了两个方法,但是不同之处在于,这两个方法并不是在所有情况下都自动生成,C++11标准中约束如下:

§ 12.8 / 9

If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared as defaulted if and only if

  • X does not have a user-declared copy constructor,
  • X does not have a user-declared copy assignment operator,
  • X does not have a user-declared move assignment operator,
  • X does not have a user-declared destructor, and
  • The move constructor would not be implicitly defined as deleted.

§ 12.8 / 20

If the definition of a class X does not explicitly declare a move assignment operator, one will be implicitly declared as defaulted if and only if

  • does not have a user-declared copy constructor,
  • does not have a user-declared move constructor,
  • does not have a user-declared copy assignment operator,
  • does not have a user-declared destructor, and
  • The move assignment operator would not be implicitly defined as deleted.

也就是说,一旦class定义了拷贝构造、拷贝赋值、析构函数、移动构造或者移动赋值,移动构造和移动赋值函数将不会自动生成.

C++14标准中更进一步,一旦显式声明了移动构造或者移动赋值,则默认将拷贝构造和拷贝赋值声明为deleted,这就意味着显式移动操作会使对象默认不可复制/赋值,基本上非常接近于编译器强制要求Rule of Five:

§ 12.8 / 7

If the class definition does not explicitly declare a copy constructor, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor.

§ 12.8 / 18

If the class definition does not explicitly declare a copy assignment operator, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy assignment operator is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy constructor or a user-declared destructor.

总结

对于C++class的声明和定义,实现时可以遵循如下简单原则:

  • 尽可能采用标准,使用智能指针和 STL,应用 Rule of Zero,避免不必要的复杂度
  • 一旦无法做到,应用 Rule of Five/ Rule of All

参考

  • Enforcing the Rule of Zero
  • Modern C++ Features – Default Initializers for Member Variables


以上是关于现代C++应用之Rule of Zero的主要内容,如果未能解决你的问题,请参考以下文章

C++ 中的 Rule-of-Three

vscode配置c++环境竟然是有手就行 !¿?!

我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印出C++的函数耗时以及代码片段耗时详情

在现代 C++ 应用程序中使用 Win32 代码时,是不是应该使用正确的转换?

蓝桥ROS机器人之现代C++学习笔记7.5 内存模型

蓝桥ROS机器人之现代C++学习笔记7.4 条件变量