现代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
不需要声明或定义析构、拷贝构造、移动构造、拷贝赋值、移动赋值等系列函数.
针对类声明或定义,随着标准的变化,发展顺序是:
-
Rule of Three
该规则指出,如果一个
class
定义了构造函数,则其几乎总是要定义拷贝构造函数和拷贝赋值操作,事实上它是两条规则: -
如果你定义了构造函数,你可能需要定义拷贝构造函数和赋值操作
-
如果你定义了拷贝构造函数或者赋值操作,那么你可能两个都需要,并且也需要实现析构函数
-
Rule of Five
相比
Rule of Three
多了移动构造、移动赋值两个函数,规则较为复杂,文章后续讲解. -
Rule of Zero
为什么开发者要绕过编译器提供自己的实现?通常有两种情况:
-
管理资源 -
多态的析构及虚函数
基于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的主要内容,如果未能解决你的问题,请参考以下文章
我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印出C++的函数耗时以及代码片段耗时详情