5

C++:Rule of five/zero 以及Rust:Rule of two

 2 years ago
source link: https://zhuanlan.zhihu.com/p/369349887
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C++:Rule of five/zero 以及Rust:Rule of two

微信公众号:CrackingOysters

C++给了程序员自由,也给了程序员更多的心智负担。

v2-55ab0818b0731e5f22f2c4044aa4b624_720w.jpg

资历浅的程序员经常纠结C++的某些规则,因为不纠结这些规则,写C++就会没有信心,从而容易犯错。

比如C++里面的构造函数、拷贝构造函数、拷贝赋值函数、移动构造函数和移动赋值函数这五个特殊的C++函数,你得分得清它们使用的规则(下文会讲这几个特殊函数是什么)。

<<Effective C++>>专为有一个item讲解这几个函数的使用规则,比如什么时候是自动生成的,什么时候是会自动delete的。资历浅的程序员,会花许多时间去研究这些规则,可是研究完还是难以应用。只有经过千锤百炼,摔进很多回坑才会懂。(大神除外)

这几个特殊的函数为了方便程序员记忆与使用,概括为两条规则:Rule of five 和Rule of zero。(C++ 11以前是rule of three) 如果你不清楚它们,并且觉得可以挑战一下,推荐阅读英文链接,也可以网上搜索资料来学习。

本文也不能让你在五分钟内掌握这两条规则,因为它们需要练习才能掌握,也需要相应的知识储备。但是本文尝试从Rust的角度去解析这两条规则,提供新的视角。

规则五和规则零

先简单总结一下C++的Rule of five和Rule of zero:

上面的五个特殊函数(为什么是特殊的函数?)要么五个显示地定义清楚,要么都不要定义。
显示地定义清楚包括提供定制化的版本,或者显示地定义为delete,或者显示地定义为default。

上面两句话总结很简练,具体使用还要参考链接和链接里面的链接。

那为什么C++规则这么多呢(戏称”事多")?因为在C++,每一个可以选择的地方,C++都会让你选。这么做,就是给予了程序员很多很多的自由,让程序员可以定制化。比如为什么要有这五个特殊的函数,因为下面的五个语句,C++认为它们表达的意思是不一样的:

  1. Widget w(w0);
  2. Widget w = w0;
  3. Widget w; w = w0;
  4. Widget w(std::move(w0));
  5. Widget w = std::move(w0);

语句一和语句二是构造一个Widget,因为w从不存在变成了存在,需要先创建,只不过借助了已有的w0,所以要调用拷贝构造函数Widget(const Widget & w0);

语句三是赋值,因为w已经存在,所以只是需要拷贝,不需要创建 。跟语句一和二本质不同。我们要调用拷贝赋值函数,不能调用拷贝构造函数。(那为啥不是移动赋值呢?)

语句四是调用移动构造函数,语句五是调用移动赋值函数。认真品品这些函数的名字,它们的差别很细微。而且写代码的时候除了名字,其他类型信息,一个字符也不能写错,不然你写的就是不一样的函数。移动构造和移动赋值,是跟C++11添加的右值语义相关。而这个又是C++规则多的另外一个表现。你可以搜索相应的资料来学习,它很有用的,这里就暂且不表。

所以上面五个语句调用的对应的函数是

  1. Widget(const Widget& w);
  2. Widget(const Widget& w);
  3. Widget &operator=(const Widget& w);
  4. Widget(const Widget&& w);
  5. Widget &operator=(const Widget&&);

这么看来,Rule of five and rule zero是不能三言两语就讲得透彻的。因为针对每种情况,C++都让你可以自定义,而且必须在特定的规则下。那为什么Rust里面没有这么多的规则呢?

答:因为Rust不给你这么多选择,不让你自定义太多。什么?你是成年人,你都要,哈哈哈。

Rust的规则

Rust没有特殊的函数叫构造函数,所有的值都是显示地初始化,比如要创建Widget,

只能这么干

let w = Widget { field的初始化}

什么Widget::new都是普通的函数,类似于工厂方法。另外Rust没有placement new,不让你在指定的buffer里面创建对象。

至于拷贝构造/赋值,移动构造/赋值四个特殊的函数,Rust说“我不需要这些函数。因为我默认就是移动,而且按我规定的方式来移动,你作为程序员不能自定义“。所以在Rust,默认是移动(move),而且默认移动后原有的变量不能被继续使用,除非变量是带有Copy trait的trivial的类型。比如

let s = "hello world".to_owned();
let new_s = s;
// println("{}", s); //uncomment this line will not compiled.

String s在第二行已经移动给了new_s,s已经不能再被继续使用。如果去掉第三行的注释,程序会被报错

error[E0382]: borrow of moved value: `s`
 --> src/main.rs:4:20
  |
2 |     let s = "ss".to_owned();
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let new_s = s;
  |                 - value moved here
4 |     println!("{}", s);
  |                    ^ value borrowed here after move

带有Copy trait的trivial类型也很好理解,就是C++/C里面的plain old object,比如int, float之类。如果你要拷贝怎么办,那么就要显示地调用clone方法。根据对比,我们也可以发现C++里面的move只是cast成右值(见下面的代码),而rust的才是人们直观理解上的move。

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

这么看来,Rust的规则简单和容易记忆理解(在你学习了以后),是因为Rust不让程序员选择太多,不让程序员自定义太多东西。如果你偏向自由,选C++,如果你偏向简单的规则,选Rust。这里的自由是编写代码可以自定义你想定制化的地方(实际程序,大部分时候都不需要定制化这些地方,除了少数情况,所以自由是大部分时候的负担)。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK