榆林市 网站建设,江苏网站建设价格低,媒体查询响应式布局,南宁网站建设索q.479185700左值、右值、左值引用、右值引用和std::move 1. 什么是左值、右值2. 什么是左值引用、右值引用3. **右值引用和std::move的应用场景**3.1 实现移动语义3.2 **实例#xff1a;vector::push_back使用std::move提高性能** **4. 完美转发 std::forward**5. Reference 写在前面vector::push_back使用std::move提高性能** **4. 完美转发 std::forward**5. Reference 写在前面 如果你也被左值、右值、左值引用、右值引用和std::move搞得焦头烂额相关概念和理解不够深入或者认识模棱两可那么这篇文章将非常的适合你耐心阅读相信一定会有所收获 1. 什么是左值、右值
左值 可以取地址、位于等号左边 – 表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)
右值 没法取地址、位于等号右边 – 表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式 便携方法对表达式取地址如果能则为左值否则为右值 int val;
val 4; // 正确 ①
4 val; // 错误 ②上述例子中由于在之前已经对变量val进行了定义故在栈上会给val分配内存地址运算符要求等号左边是可修改的左值4是临时参与运算的值一般在寄存器上暂存运算结束后在寄存器上移除该值故①是对的②是错的
2. 什么是左值引用、右值引用
引用本质是别名可以通过引用修改变量的值传参时传引用可以避免拷贝其实现原理和指针类似。
左值引用指向左值的引用称为左值引用
int a 5;
int ref_a a; // 左值引用指向左值编译通过
int ref_a 5; // 左值引用指向了右值会编译失败引用是变量的别名由于右值没有地址没法被修改所以左值引用无法指向右值。 那么const左值引用可不可以指向右值呢 可以 const int ref_a 5;const左值引用不会修改指向值因此可以指向左值和右值这也是为什么要使用const 作为函数参数的原因之一如std::vector的push_back函数原型 void push_back (const value_type val);
//如果没有constvec.push_back(5)这样的代码就无法编译通过了。
//因为5是右值右值引用右值引用的标志是可以指向右值不可以指向左值。
int ref_a_right 5; // okint a 5;
int ref_a_left a; // 编译不过右值引用不可以指向左值ref_a_right 6; // 右值引用的用途可以修改右值自然而然就会出现这样一个问题右值引用有办法指向左值吗右值引用有啥作用 有办法std::move() int a 5; // left value
int ref_a_l a; // left reference.
int ref_a_r std::move(a); //rvalue reference.
std::cout ref_a_r std::endl;左值a通过std::move移动到了右值ref_a_right中那是不是a里边就没有值了并不是打印出a的值仍然是5. std::move是一个非常有迷惑性的函数不理解左右值概念的人往往以为它能把一个变量里的内容移动到另一个变量但事实上std::move移动不了什么唯一的功能是把左值强制转化为右值让右值引用可以指向左值。其实现等同于一个类型转换static_castT(lvalue)。 所以单纯的std::move(xxx)不会有性能提升 那么左值引用、右值引用本身是左值还是右值
被声明出来的左值引用和右值引用都是左值因为他们都是有地址的也位于等号左边这符合我们刚刚的定义。
右值引用既可以是左值也可以是右值如果有名称则为左值否则是右值。作为函数返回值的 是右值直接声明出来的 是左值
左右值引用的区别
从性能上讲左右值引用没有区别传参使用左右值引用都可以避免拷贝。右值引用可以直接指向右值也可以通过std::move指向左值而左值引用只能指向左值(const左值引用也能指向右值)。作为函数形参时右值引用更灵活。虽然const左值引用也可以做到左右值都接受但它无法修改有一定局限性。
3. 右值引用和std::move的应用场景
按上文分析std::move只是类型转换工具不会对性能有好处右值引用在作为函数形参时更具灵活性看上去还是挺鸡肋的。他们有什么实际应用场景吗
3.1 实现移动语义
在实际场景中右值引用和std::move被广泛用于在STL和自定义类中实现移动语义避免拷贝从而提升程序性能。 在没有右值引用之前一个简单的数组类通常实现如下有构造函数、拷贝构造函数、赋值运算符重载、析构函数等。
class Array {
public:Array(int size) : size_(size) {data_ new int[size_];}// 深拷贝构造-当代码中有指针开辟堆内存时// 必须显式定义拷贝构造函数开辟新的堆内存存储拷贝后的指针数据// 否则两个对象的指针会指向同一个堆内存地址当某一个对象析构后// 相应的堆内存就会释放掉导致另一个对象内的指针成为悬浮指针// 浅拷贝-不涉及指针的拷贝Array(const Array temp_array) {size_ temp_array.size_;data_ new int[size_];for (int i 0; i size_; i ) {data_[i] temp_array.data_[i];}}// 深拷贝赋值 const引用避免了传参拷贝但是堆内存仍然需要深拷贝所以需要用到std::move实现移动赋值Array operator(const Array temp_array) {delete[] data_;size_ temp_array.size_;data_ new int[size_];for (int i 0; i size_; i ) {data_[i] temp_array.data_[i];}}~Array() {delete[] data_;}public:int *data_;int size_;
};该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了但是内部实现要深拷贝无法避免。 这时有人提出一个想法是不是可以提供一个移动构造函数把被拷贝者的数据移动过来被拷贝者后边就不要了这样就可以避免 深拷贝 了 关于深拷贝和浅拷贝的区别和联系后续也会出一篇文章链接在深拷贝与浅拷贝 深拷贝构造-当代码中有指针开辟堆内存时必须显式定义拷贝构造函数开辟新的堆内存存储拷贝后的指针数据否则两个对象的指针会指向同一个堆内存地址当某一个对象析构后相应的堆内存就会释放掉导致另一个对象内的指针成为悬浮指针 浅拷贝-不涉及指针的拷贝 如
class Array {
public:Array(int size) : size_(size) {data_ new int[size_];}// 深拷贝构造Array(const Array temp_array) {...}// 深拷贝赋值Array operator(const Array temp_array) {...}// 移动构造函数重载深拷贝构造函数可以浅拷贝- 形参是const 构造函数内对temp_array赋值编译不通过Array(const Array temp_array, bool move) {data_ temp_array.data_;size_ temp_array.size_;// 为防止temp_array析构时delete data提前置空其data_ temp_array.data_ nullptr;}~Array() {delete [] data_;}public:int *data_;int size_;
};这么做有2个问题
不优雅表示移动语义还需要一个额外的参数(或者其他方式)。- 重载拷贝构造函数无法实现temp_array是个const左值引用无法被修改所以temp_array.data_ nullptr;这行会编译不过。当然函数参数可以改成非constArray(Array temp_array, bool move){...}这样也有问题由于左值引用不能接右值Array a Array(Array(), true);这种调用方式就没法用了。
可以发现左值引用真是用的很不爽右值引用的出现解决了这个问题在STL的很多容器中都实现了以 右值引用为参数的移动构造函数和移动赋值重载函数或者其他函数最常见的如std::vector的push_back和emplace_back。参数为左值引用意味着拷贝为右值引用意味着移动。
class Array {
public:......// 优雅Array(Array temp_array) {data_ temp_array.data_;size_ temp_array.size_;// 为防止temp_array析构时delete data提前置空其data_ temp_array.data_ nullptr;}public:int *data_;int size_;
};如何判断一个对象是否是可以移动的 在C中是否可以移动一个对象取决于该对象的类是否定义了移动构造函数或移动赋值运算符。 以下是一些判断一个对象是否可以被移动的方法 检查类的定义如果一个类定义了移动构造函数或移动赋值运算符那么这个类的对象就可以被移动。移动构造函数和移动赋值运算符的声明通常如下 class MyClass {
public:MyClass(MyClass other); // 移动构造函数MyClass operator(MyClass other); // 移动赋值运算符// ...
};注意这两个函数的参数都是右值引用。 使用std::is_move_constructible和std::is_move_assignable这两个模板在type_traits头文件中定义可以用来检查一个类型是否有可用的移动构造函数或移动赋值运算符 std::cout std::is_move_constructibleMyClass::value std::endl; // 如果MyClass可移动输出1否则输出0
std::cout std::is_move_assignableMyClass::value std::endl; // 如果MyClass可移动赋值输出1否则输出0使用std::move_if_noexcept这个模板函数可以用来判断是否可以无异常地移动一个对象。如果移动操作可能抛出异常它会选择拷贝操作。这在一些容器操作中非常有用例如std::vector的重新分配。 需要注意的是即使一个对象可以被移动也不意味着应该总是移动它。在某些情况下例如当你知道一个对象将在移动操作后立即被销毁或者你想避免昂贵的拷贝操作时移动是有意义的。在其他情况下移动可能会导致难以追踪的错误例如如果你错误地移动了一个仍然需要使用的对象。 3.2 实例vector::push_back使用std::move提高性能
// std::move会调用到移动语义函数避免了深拷贝。
int main() {std::string str1 aacasxs;std::vectorstd::string vec;vec.push_back(str1); // 传统方法copyvec.push_back(std::move(str1)); // 调用移动语义的push_back方法避免拷贝str1会失去原有值变成空字符串vec.emplace_back(std::move(str1)); // emplace_back效果相同str1会失去原有值vec.emplace_back(axcsddcas); // 当然可以直接接右值
}// std::vector方法定义
void push_back (const value_type val);
void push_back (value_type val); // 内部调用了emplace_backvoid emplace_back (Args... args);可移动对象在需要拷贝且被拷贝者之后不再被需要的场景建议使用std::move触发移动语义提升性能。
moveable_objecta moveable_objectb;
改为
moveable_objecta std::move(moveable_objectb);还有些STL类是move-only的比如unique_ptr这种类只有移动构造函数因此只能移动(转移内部对象所有权或者叫浅拷贝)不能拷贝(深拷贝):
std::unique_ptrA ptr_a std::make_uniqueA();std::unique_ptrA ptr_b std::move(ptr_a); // unique_ptr只有‘移动赋值重载函数‘参数是 只能接右值因此必须用std::move转换类型std::unique_ptrA ptr_b ptr_a; // 编译不通过std::move本身只做类型转换对性能无影响。 我们可以在自己的类中实现移动语义避免深拷贝充分利用右值引用和std::move的语言特性。
4. 完美转发 std::forward
和std::move一样它的兄弟std::forward也充满了迷惑性虽然名字含义是转发但他并不会做转发同样也是做类型转换.
与move相比forward更强大move只能转出来右值forward都可以。 std::forward(u)有两个参数T与 u。 a. 当T为左值引用类型时u将被转换为T类型的左值 b. 否则u将被转换为T类型右值。 举个例子有mainAB三个函数调用关系为main-A-B建议先看懂2.3节对左右值引用本身是左值还是右值的讨论再看这里
void B(int ref_r) {ref_r 1;
}// A、B的入参是右值引用
// 有名字的右值引用是左值因此ref_r是左值
void A(int ref_r) {B(ref_r); // 错误B的入参是右值引用需要接右值ref_r是左值编译失败B(std::move(ref_r)); // okstd::move把左值转为右值编译通过B(std::forwardint(ref_r)); // okstd::forward的T是int类型属于条件b因此会把ref_r转为右值
}int main() {int a 5;A(std::move(a));
}例2
void change2(int ref_r) {ref_r 1;
}void change3(int ref_l) {ref_l 1;
}// change的入参是右值引用
// 有名字的右值引用是 左值因此ref_r是左值
void change(int ref_r) {change2(ref_r); // 错误change2的入参是右值引用需要接右值ref_r是左值编译失败change2(std::move(ref_r)); // okstd::move把左值转为右值编译通过change2(std::forwardint (ref_r)); // okstd::forward的T是右值引用类型(int )符合条件b因此u(ref_r)会被转换为右值编译通过change3(ref_r); // okchange3的入参是左值引用需要接左值ref_r是左值编译通过change3(std::forwardint (ref_r)); // okstd::forward的T是左值引用类型(int )符合条件a因此u(ref_r)会被转换为左值编译通过// 可见forward可以把值转换为左值或者右值
}int main() {int a 5;change(std::move(a));
}上边的示例在日常编程中基本不会用到std::forward最主要运于模版编程的参数转发中想深入了解需要学习万能引用(T )和引用折叠(eg: → ?)等知识本文就不详细介绍这些了。
5. Reference
https://zhuanlan.zhihu.com/p/374392832
https://zhuanlan.zhihu.com/p/335994370
https://www.cnblogs.com/shadow-lr/p/Introduce_Std-move.html