
变量的赋值有两种语义,move和copy,C++和Rust作为系统级的编程语言,都实现了这两种语义,不过在表现形式上稍有不同。这里通过这两种语言的对比简单总结下C++和Rust中move和copy的不同,以及他们对函数签名函数传参的影响,希望能对大家理解move和copy有所帮助。
一、rust中的move和copy在rust中,栈上的值的赋值操作默认是copy语义(实现了Copy trait):
let x = 5;
let y = x;
println!("x {} y {}", x, y);
上面的代码中,y是x的一个copy,copy之后,x和y都可以访问,上面的代码输出x 5 y 5。较为复杂的堆上的对象,赋值操作默认是move语义:
如上面的代码,nums1通过赋值操作move给了nums2,转移了对这个值的ownership,这样nums1就不可以再访问了,上面第3行代码会报borrow of moved value: 'nums1'的编译错误。修复上面编译报错有两种方法,一种是显示的clone:
let nums1 = vec![1, 2, 3];
let nums2 = nums1.clone();
println!("nums1 {:?} nums2 {:?}", nums1, nums2);
另外一种是borrow:
let nums1 = vec![1, 2, 3];
let nums2 = &nums1;
println!("nums1 {:?} nums2 {:?}", nums1, nums2);
上面这两种方法都会输出nums1 [1, 2, 3] nums2 [1, 2, 3]。第一种方法nums1.clone()显示的把nums1复制了一份,这样并不影响nums1的ownership,复制完成之后,两个值是相互独立的;第二种方法&nums1是对nums1的一个引用,从语义上来说是对nums1这个值的借用。另外,rust在编译期检查了引用不能超过值的lifetime,保证引用的有效性,不会存在值已经释放而引用还在(dangling reference)的场景;同时,rust还对borrow做了限制,可修改的borrow和普通的只读的borrow不能同时存在,防止data race的产生。
通过上面的例子可以看出,在rust中,比较小的存储在栈上的值的赋值操作默认是copy的,大的存储在堆上的较为复杂的值默认是move的,并且move之后,原来的变量不可再访问,如果要实现复制语义的话,需要显示的调用clone()方法。
二、C++中的move和copy在C++中move和copy理解起来要稍微复杂一些,C++中分为值类型和引用类型,值分为左值和右值,同样的引用也分为左值引用和右值引用,左值引用和右值引用本质上类似于指针,和指针的一个不同是指针可以指向空nullptr,而引用在声明时一定要初始化,不允许空的引用。C++中的左值和右值的概念来自于C语言,能在等式左边的是左值(能取其的地址)、只能在等式右边出现的是右值(常量、临时变量比如说s1 + s2等)。
int var = 42; int& ref = var; ref = 99; assert(var == 99);
如上面的代码,var是一个左值,ref是它的一个引用,针对这个引用的修改会修改原始的变量。但是下面的代码会报错:
int& ref1; // ‘ref1’ declared as reference but not initialized int& ref2 = 4; // cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
4是一个临时值,只能出现在等式的右边,因此是一个右值,左值的引用不能绑定到右值上。但是下面是可以的:
const int& ref = 4;
C++中允许const的左值引用绑定到右值,这应该是C++的一个特例,这样才能在调用void print(const std::string& s);函数的时候传递临时的值print("hello"); 。
int&& i = 42; int j = 42; int&& k = j; // cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
如上面的代码,int&&定义了右值引用类型,上面的第3行,j是一个左值,右值引用不能绑定到左值,上面的代码会报cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’编译错误,要修复这个错误,可以通过std::move把一个左值转成一个右值,上面的代码修改成:
int&& i = 42; int j = 42; int&& k = std::move(j);
注意:std::move并没有移动任何东西,上面的代码中,std::move类似于static_cast
#includevoid test(std::vector && nums) { // TODO } int main() { std::vector nums = {1,2,3}; test(std::move(nums)); return 0; }
比如说上面的代码,std::move把一个左值的nums转成了右值,语义上是说这个值传递给test函数的时候,test函数内部可以对这个值做任意的操作,比如说把它移动走,也可以不把它移动走,完全取决于test函数的实现,test函数调用之后,nums值变成了什么呢?什么值都有可能,如果test函数内部把它移动走了的话,nums就变成了空的状态,如果没有移动,甚至test内部直接对nums做了修改的话,nums就变成了修改后的值(右值引用本质上也类似于指针哈~),所以这里建议一个变量的值被std::move转成右值之后,这个变量就不要再使用了,除非对这个变量重新进行赋值。另外需要注意的是在test函数中,如果要把nums的内容移走的话,要再加一次std::move才行:
void test(std:vector&& nums) { std::vector nums1 = nums; std::vector nums2 = std::move(nums); }
如上面的代码,nums到nums1的赋值走的是拷贝构造函数,而下面的nums2走的才是移动构造函数,这是由于nums作为函数的参数是有地址的,是左值,虽然它的类型是右值引用。
注意:在代码中要尽量避免self move,可以试一试下面的代码会输出什么?
#include#include int main() { std::vector nums = {1,2,3}; nums = std::move(nums); for (auto i : nums) { std::cout << i << std::endl; } return 0; }
另外在C++中,函数模板的参数如果是T&&的话,比如下面的例子:
templatevoid foo(T&& t) {}
如果传入的是左值的话,t会被实例化成左值的引用类型,如果传入的是右值的话,t会被实例化成右值的引用类型,借此可以用来实现完美转发。
三、如何设计函数的参数类型在rust中比较简单,如果要consume这个参数的话就直接传值,如果要borrow的话函数的参数类型就是引用,如果要修改传入的参数的话就mut borrow。C++中比较复杂,函数参数的类型可以是普通的值类型、const左值引用、普通左值引用、右值引用、指针等,函数的参数到底用哪种类型好呢,这是一个复杂的问题,可以参考这里:
https://isocpp.github.io/CppCor
这里就不再过多的说明了。
总之,C++和Rust都能实现move和copy语义,但是在使用上有一些区别。
附录 一、源码编译安装最新版本gcc操作步骤从gcc官网下载源码包gcc-12.1.0.tar.xz并放置到~/gcc-building/目录下,然后执行下面的一系列的编译安装配置命令:
cd ~/gcc-building/ && tar xvf ./gcc-12.1.0.tar.xz cd ~/gcc-building/gcc-12.1.0 && ./contrib/download_prerequisites # 配置gcc,如果是在centos 7环境下,要兼容系统默认安装的gcc 4.8.5的话, # 可以加上`--with-default-libstdcxx-abi=gcc4-compatible` mkdir ~/gcc-building/building && cd ~/gcc-building/building ../gcc-12.1.0/configure --prefix=/opt/gcc-12.1.0 --disable-multilib --enable-languages=c,c++ # 编译并安装 cd ~/gcc-building/building && make -j cd ~/gcc-building/building && make install ## 配置环境变量 export PATH=/opt/gcc-12.1.0/bin:$PATH export CC=/opt/gcc-12.1.0/bin/gcc export CXX=/opt/gcc-12.1.0/bin/g++ ## 配置ldconfig echo -e "/opt/gcc-12.1.0/libn/opt/gcc-12.1.0/lib64" > /etc/ld.so.conf.d/gcc-12.1.0-x86_64.conf
参考资料:
《C++ Concurrency in Action, Second Edition》
https://en.cppreference.com/w/c
还有更多免费资料可以领取哦!加q:【828339809】获取