移动语义:意味着把某对象持有的资源或内容转移给另一个对象。
右值引用:只能在赋值运算符的右侧使用,没有名字的变量,不能取地址,不能解引用
纯右值(prvalue),将亡值(xvalue),和左值(lvalue)
C++语法中把表达式分为三种值类型,分别是:纯右值(prvalue),将亡值(xvalue),和左值(lvalue)。
左值有:有名字的变量; 可以取地址的变量; 指针解引用的值(使用语言内置的*或->运算符);
右值有:字面值(literal),比如1, true, nullptr——但字符串常量不是右值而是左值,它们被编译器放在数据段(data segment),可以取地址;非引用类型的函数返回值;各种内置的运算符组合表达式的计算结果。
右值引用:表达式等号右边的值需要是右值,可以使⽤std::move函数强制把左值转换为右值。
左值引用:并不拥有所绑定对象的堆存,所以必须⽴即初始化。 对于左值引⽤,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使⽤const引⽤形式
template <typename T>
struct my_collection
{
std::vector<T> collection_;
void add(T thing) // 按值传递
{
collection_.push_back(thing);
}
void add(const T& thing) // 按常量左值引用传递
{
collection_.push_back(thing);
}
void add(T&& thing) // 按右值传递
{
collection_.push_back( std::move(thing) );
}
};
1、T&& thing不一定是右值引用:为什么参数为右值时,还要再std::move()一回?如上文所言,区分左右值的标准之一就是变量是否有名字。thing虽然是个右值引用(type),但它有名字,因此仍然是左值(value category);如果要移动它,就得调用std::move()把它转换为右值引用。
2、const T&&会失去移动语义,因为移动需会修改对象 :可以写const T&&吗?这个引用类型确实是合乎语法的,但是语义上存在矛盾:移动会修改对象,const修饰符又禁止了修改。因此,如果真的传入一个const T&&给add(T&& thing),那么将调用push_back(const T&)而不是push_back(T&&),从而失去移动语义。这里可以看到,右值引用是可以绑定到左值引用的。对于已有复制构造(赋值)函数,但没有重载移动构造(赋值)函数的类,传递右值引用作为参数会回退为调用复制构造(赋值)函数。
3、std::move()不移动任何东西,只是明确地表达移动的意图:放弃该对象所持有资源的所有权,转移给其他对象。
移动语义
移动语义的引入:C++11引入了移动语义,主要目的是提高程序的效率,尤其是对于涉及资源管理(如内存分配)的类。当一个对象以右值的形式出现时(通常是临时对象或被std::move处理过的对象),理想情况下应该使用移动构造函数或移动赋值操作符来**“移动”资源**,而非进行耗时的深拷贝。
移动语义的“移动”,意味着把某对象持有的资源或内容转移给另一个对象。以转移一个vector对象为例,移动构造red_vector时,并没有把原来的数组内容复制一遍,而是直接把指针指向orange_vector的数组,然后把orange_vector的指针置空,再设置red_vector的大小为该数组的大小
移动语义实现:
// 移动构造函数
// std::exchange(obj, new_val)的作用是把返回obj的旧值,并把new_val赋值给obj
my_vector(my_vector&& oth) :
data_(std::exchange(oth.data_, nullptr)),
size_(std::exchange(oth.size_, 0)),
capacity_(std::exchange(oth.capacity_, 0))
{}
C++提供了表达运行时错误的异常机制,可以通过throw关键字来抛出异常对象;相应地,在使用异常机制的语境下,发展出了“异常安全”的概念。比如,往一个std::vector对象中添加一个新元素时,如果其预先分配的数组已满,其会申请一块更大的内存;但如果内存耗尽,内存分配器抛出异常std::bad_alloc。根据标准,std::vector申请新内存失败后,原有数组应保持不变,这被称为strong exception guarantee。
void reallocate() {
size_t new_capacity = capacity_ * 2;
// 申请内存:内存不足时会抛异常!
T* new_data = allocate(sizeof(T) * new_capacity);
// 移动或复制已有元素到新数组
T* first = data_;
T* last = data_ + size_;
if ( can_nothrow_move_construct<T> ) { // 如果T的移动构造函数不抛出异常,则移动构造
move_construct(first, last, new_data); // 在new_data所指向的内存上移动构造[first, last)范围的元素
}
else { // 否则复制构造
try {
T* constructed_first = new_data;
T* constructed_last = new_data;
while (first != last) {
copy(first++, constructed_last++); // 复制构造
}
}
catch (...) { // 如果复制构造抛出异常:
destroy(constructed_first, constructed_last); // 析构新数组中已经构造的元素
free(new_data); // 释放内存
throw; // 重新抛出异常给vector调用者
}
}
}
假设:移动第n个元素到新数组时,T的移动构造抛出异常,就会导致新数组中有n - 1个完好的元素,第n个元素状态未定义;而旧数组中则有n - 1个被移动过的元素,1个未定义的元素,还可能一些剩下未移动的元素。这时,vector的状态就难以恢复了。因此,为了保证异常安全,如果类型T的移动构造函数不应该抛出异常,否则,vector在扩容时仍然会复制,而不是移动元素!
在C++中,可以通过noexcept符号声明一个函数不会抛出异常。编译器不会给一个noexcept函数生成异常处理代码,并会检查该函数中调用的其他函数是否也是noexcept的。
unique_ptr的“复制”(unique_ptr禁止拷贝构造函数、拷贝赋值运算符,但实现了移动构造函数、移动赋值运算符用来高效转移std::unique_ptr 的所有权。)
template <typename T>
class my_unique_ptr
{
T* ptr_;
public:
// 移动构造函数
my_unique_ptr(my_unique_ptr&& oth) noexcept;
// 移动赋值函数
my_unique_ptr& operator=(my_unique_ptr&& rhs) noexcept;
};
小结一下
移动语义让程序员在复制对象时有所选择,不一定需要发生深拷贝。
右值引用是C++语法层面表示移动语义的机制。
std::move()没有移动任何东西,只是把一个表达式转型(cast)为右值。
尽量让具有移动语义的函数noexcept。