C++ 左值与右值
2019-08-21 15:11:51
【右值】
Preview:在 C++11 中引入了右值以及移动语义,其核心目的是为了减少无谓的复制,提高效率。
Concept:任何一个 C++ 都可分为左值和右值,广义的理解为:
(1)左值为持久化的对象,表达式结束后仍然存在。
(2)右值为临时对象,没有名字,在表示式结束后析构。
狭义理解:可以取地址的对象为左值。
Addition:
1. 返回左值的表达式:赋值,下标,解引用,前置递增/递减运算符
2. 返回右值的表达式:算术,关系,位,后置递增/递减运算符,函数返回的普通临时变量
【右值引用】
Concept:右值引用只能绑定到一个将要被销毁的对象(即:没有其他用户),而且只能绑定右值。
(同样,左值引用也只能绑定到一个左值,但是常量左值引用是个特例,其可以绑定任何值。)
Addition:引用会发生折叠,只有两个右值引用会合成一个右值引用,其他折叠都会生成左值引用。
X& & -> X&
X& && -> X&
X&& & -> X&
X&& && -> X&&
因此,模板类右值引用能够接受任何种类的参数:
template<typename T>
void func(T&&);
通常我们可以做如下重载:
template<typename T> void f(T&&); // 绑定非const右值 template<typename T> void f(const T&); // 绑定左值和const右值
【完美转发】
有了上述的万能引用,我们可以定义出完美转发函数:
template<typename T> void perfect_forward(T && t) { other_function(std::forward<T> (t)); }
该函数可以实现将参数 t 的原有特性完美保留并转发。
这里的 std::forward() 函数起到的作用:如果参数 t 原先绑定到一个右值,则 std::forward<T>(t) 也返回一个右值。如果不用 forward,直接调用 other_function(t),t 将被作为一个左值(因为其已经有名字)。
【移动语义】
- **std::move 函数**
对一个左值,像右值一样处理它,但是这就意味着,除了对 rr1 赋值或销毁之外,我们将不再使用它。
更朴素的理解:将内存控制权转移(放弃)。
int rr1 = 42; int &&r3 = std::move(rr1);
实现方法:
template <typename T>
class remove_reference{
public:
typedef T type;
};
template<typename T> typename remove_reference<T>::type&& move(T&& t){ return static_cast<typename remove_reference<T>::type&&>(t); }
【移动构造函数】
使用移动构造函数,所管理的内存将不会被拷贝,而是直接接管内存的所有权。(移交管理权)
Plus:
rule1:如果类定义了一个移动构造函数和移动赋值运算符,则该类的合成拷贝构造函数和合成拷贝赋值运算符会被定义为删除的。
rule2:如果类定义了自己的拷贝构造函数,拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。
例子(自己实现一个 vector 类,其中的移动构造函数):
template<typename T> class Vec{ public: Vec(): elements(nullptr), first_free(nullptr), cap(nullptr) {} // 拷贝构造函数 Vec(const Vec<T>&); // 拷贝赋值运算符 Vec<T>& operator = (const Vec<T>&); // 析构函数 ~Vec(); // 移动构造函数 Vec(Vec<T>&&) noexcept; // 需要指定 noexcept,否则编译器不用移动构造 // 移动赋值运算符 Vec<T>& operator = (Vec<T>&&) noexcept; size_t size() const { return first_free - elements; } size_t capacity() const { return cap - elements; } T* begin() const { return elements; } T* end() const { return first_free; } void push_back(const T&); void push_back(T&&); // ... private: Static std::allocator<T> alloc; // 被添加元素的函数所使用 void chk_n_alloc(){ if(size() == capacity()) reallocate(); } // 工具函数 std::pair<T*, T*> alloc_n_copy(const T*, const T*); void free(); // 销毁元素并释放内存 void reallocate(); // 获取更多内存并拷贝已有元素 T* elements; // 指向首元素的指针 T* first_free; // 指向第一个空间元素的指针 T* cap; // 指向尾巴后一个位置的指针 }; // 移动构造函数 template<typename T> Vec::Vec(Vec<T>&& s) noexcept // 移动操作不应抛出任何异常,C++11 // 接管内存 :elements(s.elements), first_free(s.first_free), cap(s.cap) { // 令 s 进入一个状态:对其运行析构函数是安全的 // 不会析构掉 elements s.elements = s.first_free = s.cap = nullptr; } // 移动赋值运算符,要正确处理自赋值 template<typename T> Vec<T>& Vec::operator = (Vec<T>&& copy) noexcept{ // 检测自赋值 if(this != ©){ free(); elements = copy.elements; first_free = copy.first_free; cap = copy.cap; // 将 copy 置为可析构状态 copy.elements = copy.first_free = copy.cap = nullptr; } return *this; }
Addition:在 vector 实现中,reallocate() 函数中也使用到了移动操作。

浙公网安备 33010602011771号