Skip to content

C++11 移动构造(move constructor)函数概述

C++11中容器通过移动构造函数转移所有权给另一个容器变量

C++11 为许多标准库容器引入了移动构造函数,这意味着我们可以高效地将一个容器的所有权转移到另一个容器,而无需进行深度拷贝。以下是一些使用移动构造函数转移容器所有权的例子:

  1. std::vector
 1#include <vector>
 2#include <iostream>
 3
 4int main() {
 5    std::vector<int> vec1 = {1, 2, 3, 4, 5};
 6    
 7    // 使用移动构造函数,转移 vec1 的所有权到 vec2
 8    std::vector<int> vec2(std::move(vec1));
 9    
10    std::cout << "vec1 size: " << vec1.size() << std::endl;  // Output: vec1 size: 0
11    std::cout << "vec2 size: " << vec2.size() << std::endl;  // Output: vec2 size: 5
12}
  1. std::string
 1#include <string>
 2#include <iostream>
 3
 4int main() {
 5    std::string str1 = "Hello, world!";
 6    
 7    // 使用移动构造函数,转移 str1 的所有权到 str2
 8    std::string str2(std::move(str1));
 9    
10    std::cout << "str1: " << str1 << std::endl;  // Output: str1: 
11    std::cout << "str2: " << str2 << std::endl;  // Output: str2: Hello, world!
12}
  1. std::map
 1#include <map>
 2#include <iostream>
 3
 4int main() {
 5    std::map<int, std::string> map1 = {{1, "one"}, {2, "two"}, {3, "three"}};
 6    
 7    // 使用移动构造函数,转移 map1 的所有权到 map2
 8    std::map<int, std::string> map2(std::move(map1));
 9    
10    std::cout << "map1 size: " << map1.size() << std::endl;  // Output: map1 size: 0
11    std::cout << "map2 size: " << map2.size() << std::endl;  // Output: map2 size: 3
12}

在这些例子中,你可以看到原始容器在移动操作后变为空,而新容器则获得了原始容器的所有内容。这是因为移动操作只是转移了数据的所有权,而不是创建数据的深度拷贝。这通常比深度拷贝更有效率,特别是在处理大型数据结构时。

背后的实现原理是什么

C++11中容器的移动构造函数和移动赋值操作的实现原理主要基于新引入的右值引用。以下是移动语义的实现原理的概述:

  1. 右值引用: 在C++11之前,我们只有左值引用。C++11引入了右值引用,表示对将要销毁的对象的引用。右值引用使用&&表示。例如:int&& rvalueRef = 5;。其关键在于右值引用允许我们改变其所引用的对象,这是左值引用所不允许的。

  2. 标准库中的移动构造函数和移动赋值操作符: 对于标准库容器,它们被重新设计以包含移动构造函数和移动赋值操作符。这些函数接收一个右值引用参数,允许它们从该参数"窃取"资源,而无需复制资源。

  3. 实现: 以std::vector为例,它通常在内部维护三个指针:一个指向数据的开始,一个指向数据的末尾,一个指向已分配存储的末尾。在复制构造函数中,它会分配新的内存并复制数据。但在移动构造函数中,它只需要复制这三个指针,并重置源向量的这三个指针,确保源向量不会在其析构函数中释放数据。这是一个常数时间的操作,与数据的大小无关。

例如,std::vector的简化移动构造函数可能看起来像这样:

1template <typename T>
2vector<T>::vector(vector<T>&& other) noexcept
3    : start(other.start), end_of_elements(other.end_of_elements), end_of_storage(other.end_of_storage)
4{
5    other.start = other.end_of_elements = other.end_of_storage = nullptr;
6}
  1. std::move函数std::move是一个标准库提供的工具,将其参数转换为右值。它实际上不会移动任何内容,而只是返回一个右值引用。这使得我们可以显式地要求使用移动构造函数或移动赋值操作符,而不是复制操作。

  2. 析构: 当容器对象被销毁时,它会释放其资源。但是,由于移动操作只是交换了资源的所有权,所以我们必须确保原始对象不会再其析构函数中释放资源。这通常是通过在移动操作后将原始对象重置为一个安全状态来实现的。

总之,移动语义的核心在于重新分配资源的所有权,而不是复制资源。这是通过使用右值引用、特殊的移动构造函数和移动赋值操作符来实现的。

如果一个类没有移动构造函数,那么在调用std::move的时候会发生什么

如果一个类没有提供移动构造函数(或移动赋值操作符)并且你尝试对它使用std::move,以下是会发生的事情:

  1. 退回到拷贝:如果这个类有一个有效的拷贝构造函数(或拷贝赋值操作符),那么它会被调用。这意味着在这种情况下,使用std::move并不会产生任何性能上的优势,因为实际上进行的是深拷贝操作而不是移动。

  2. 编译错误:如果这个类既没有提供移动构造函数也没有提供拷贝构造函数(例如,拷贝构造函数被显式地删除),那么尝试使用std::move将导致编译错误。

例如,考虑以下的类:

1class NonMovable {
2public:
3    NonMovable() = default;
4    NonMovable(const NonMovable&) = default; // copy constructor
5    NonMovable& operator=(const NonMovable&) = default; // copy assignment operator
6    // Note: No move constructor or move assignment operator
7};

在这种情况下:

1NonMovable nm1;
2NonMovable nm2 = std::move(nm1); // This will use the copy constructor

总之,当类没有移动构造函数时,使用std::move会导致类的拷贝构造函数被调用(如果存在的话)。如果类没有拷贝构造函数,尝试这样做会导致编译错误。

编译器会自动为没有提供移动构造函数的类实现一个默认的移动构造函数么

编译器可能为类自动生成默认的移动构造函数,但这取决于某些条件。下面是规则的概述:

  1. 如果没有声明移动构造函数,并且以下所有情况都满足,编译器会为你的类自动生成一个默认的移动构造函数

    • 你没有提供拷贝构造函数。
    • 你没有提供拷贝赋值操作符。
    • 你没有提供析构函数。
  2. 如果你为类提供了上述中的任何一个(拷贝构造函数、拷贝赋值操作符或析构函数),编译器不会为你的类生成默认的移动构造函数。

  3. 如果你显式地删除或声明移动构造函数,编译器当然也不会自动生成一个。

  4. 如果类的某个成员对象不可移动(例如,它的移动构造函数被删除或私有化),那么编译器不会为这个类生成默认的移动构造函数。

这些规则设计的目的是保持类的行为的一致性。例如,如果你为类提供了自定义的拷贝操作或析构函数,编译器假定你有你自己的资源管理策略,因此它不会自动为你生成移动操作,避免可能的错误或不一致的行为。

如果你希望类具有移动语义并且满足上述条件,但编译器没有为你生成默认的移动构造函数,你可以显式地声明它:

1class MyClass {
2public:
3    MyClass(MyClass&&) = default; // Use compiler-generated move constructor
4    // ... other members ...
5};

通过这种方式,你可以明确地告诉编译器你希望使用默认的移动构造函数。

Related Posts

  1. effective_modern_c++ Item24_P156_1
  2. effective_modern_c++ Item24_P158_1