文章目录
- 一、持有资源的类定义移动构造函数的要点
- 1.普通内置类型与std::move
- 2.常见的容器与std::move
- 3.结构体:
- 4.智能指针与std::move
- 参考
一、持有资源的类定义移动构造函数的要点
1.普通内置类型与std::move
在C++中,std::move 主要用于对象的移动语义,对于大多数内置类型(如整数),移动语义实际上没有意义。对于内置类型(如整数、浮点数等),移动与复制的成本是相同的,因为它们的大小是固定且已知的,且复制的成本非常低廉。因此,使用 std::move 对这些类型没有实际的优化效果。
尽管如此,使用 std::move 来处理整数变量是完全合法的,但它不会带来任何性能上的优势。下面是一个简单的例子:
#include <iostream>
#include <utility> // for std::movevoid printAndMove(int &&num) {std::cout << "Value: " << num << std::endl;
}int main() {int a = 42;printAndMove(std::move(a));// a 的值仍然是 42,因为对于内置类型没有“移动”语义std::cout << "Value of a after move: " << a << std::endl;return 0;
}
总结:
- 对于像整数这样的内置类型,使用 std::move 没有实际的性能收益。
- 对于更复杂的类型(如 std::string、std::vector 等),std::move 可以避免昂贵的复制操作,通过转移资源提高性能。
2.常见的容器与std::move
#include <iostream>
#include <string>
#include <utility> // for std::moveint main() {std::string original = "Hello, world!";std::string moved_to = std::move(original);std::cout << "Moved-to string: " << moved_to << std::endl;std::cout << "Original string after move: " << original << std::endl;return 0;
}
Program returned: 0
Program stdout
Moved-to string: Hello, world!
Original string after move:
在这段代码中:
- 创建并初始化一个名为 original 的 std::string,内容为 “Hello, world!”。
- 使用 std::move 函数将 original 转换为右值引用,从而允许内容被移动到 moved_to。
- 移动之后,moved_to 包含 “Hello, world!”,而 original 处于有效但未指定的状态。通常来说,这意味着 original 变为空字符串。
3.结构体:
对于普通结构体,std::move 可以有效地将资源从一个对象转移到另一个对象。这对于结构体内部包含动态分配的资源(如动态数组或其他需要管理内存的成员变量)时尤为重要。在这种情况下,通过使用 std::move,可以避免昂贵的复制操作,提高性能。
#include <iostream>
#include <vector>
#include <utility> // for std::movestruct MyStruct {std::vector<int> data;MyStruct(std::vector<int> d) : data(std::move(d)) {}// 移动构造函数MyStruct(MyStruct&& other) noexcept : data(std::move(other.data)) {std::cout << "Move constructor called" << std::endl;}// 移动赋值运算符MyStruct& operator=(MyStruct&& other) noexcept {if (this != &other) {data = std::move(other.data);std::cout << "Move assignment operator called" << std::endl;}return *this;}// 禁用复制构造函数和复制赋值运算符MyStruct(const MyStruct&) = delete;MyStruct& operator=(const MyStruct&) = delete;
};int main() {MyStruct s1(std::vector<int>{1, 2, 3, 4, 5});// 使用 std::move 将 s1 转移到 s2MyStruct s2 = std::move(s1);std::cout << "s1 data size after move: " << s1.data.size() << std::endl;std::cout << "s2 data size after move: " << s2.data.size() << std::endl;MyStruct s3(std::vector<int>{6, 7, 8, 9, 10});// 使用 std::move 将 s3 转移到 s2s2 = std::move(s3);std::cout << "s3 data size after move: " << s3.data.size() << std::endl;std::cout << "s2 data size after move assignment: " << s2.data.size() << std::endl;return 0;
}
Program returned: 0
Program stdout
Move constructor called
s1 data size after move: 0
s2 data size after move: 5
Move assignment operator called
s3 data size after move: 0
s2 data size after move assignment: 5
对于仅包含普通内置类型的结构体,std::move 虽然是合法的,但实际上没有什么效果,因为内置类型的移动与复制是一样的。内置类型(如 int、double 等)的复制开销很低,并且移动这些类型不会带来性能提升。
但是,如果你定义了移动构造函数和移动赋值运算符,可以确保你的结构体在更复杂的情况下(例如成员类型改变)也能正确处理移动语义。
#include <iostream>
#include <utility> // for std::movestruct MyStruct {int a;double b;// 默认构造函数MyStruct(int x, double y) : a(x), b(y) {}// 移动构造函数MyStruct(MyStruct&& other) noexcept : a(other.a), b(other.b) {std::cout << "Move constructor called" << std::endl;// 这里可以将其他对象的值重置,但通常没有必要}// 移动赋值运算符MyStruct& operator=(MyStruct&& other) noexcept {if (this != &other) {a = other.a;b = other.b;std::cout << "Move assignment operator called" << std::endl;// 这里可以将其他对象的值重置,但通常没有必要}return *this;}// 禁用复制构造函数和复制赋值运算符MyStruct(const MyStruct&) = delete;MyStruct& operator=(const MyStruct&) = delete;
};int main() {MyStruct s1(42, 3.14);// 使用 std::move 将 s1 转移到 s2MyStruct s2 = std::move(s1);std::cout << "s1.a after move: " << s1.a << std::endl; // 值仍然存在,但不再关心std::cout << "s1.b after move: " << s1.b << std::endl; // 值仍然存在,但不再关心std::cout << "s2.a after move: " << s2.a << std::endl;std::cout << "s2.b after move: " << s2.b << std::endl;MyStruct s3(100, 2.71);// 使用 std::move 将 s3 转移到 s2s2 = std::move(s3);std::cout << "s3.a after move: " << s3.a << std::endl; // 值仍然存在,但不再关心std::cout << "s3.b after move: " << s3.b << std::endl; // 值仍然存在,但不再关心std::cout << "s2.a after move assignment: " << s2.a << std::endl;std::cout << "s2.b after move assignment: " << s2.b << std::endl;return 0;
}
Move constructor called
s1.a after move: 42
s1.b after move: 3.14
s2.a after move: 42
s2.b after move: 3.14
Move assignment operator called
s3.a after move: 100
s3.b after move: 2.71
s2.a after move assignment: 100
s2.b after move assignment: 2.71
4.智能指针与std::move
在C++中,std::move 对于智能指针(如 std::unique_ptr 和 std::shared_ptr)非常有用,因为智能指针管理动态分配的资源,如内存。通过使用 std::move,可以将智能指针的所有权从一个对象转移到另一个对象,而无需复制底层资源。这有助于避免资源泄漏并确保资源的唯一所有权。
#include <iostream>
#include <memory> // for std::unique_ptr and std::make_uniquevoid processUniquePtr(std::unique_ptr<int> ptr) {std::cout << "Processing unique pointer with value: " << *ptr << std::endl;
}int main() {std::unique_ptr<int> myPtr = std::make_unique<int>(42);// 使用 std::move 转移 myPtr 的所有权到 processUniquePtrprocessUniquePtr(std::move(myPtr));// myPtr 此时不再拥有资源,应该为 nullptrif (myPtr == nullptr) {std::cout << "myPtr is now nullptr after move" << std::endl;}return 0;
}
在这个例子中:
- 创建了一个 std::unique_ptr 类型的智能指针 myPtr,并使用 std::make_unique 进行初始化,指向一个值为 42 的整数。
- 使用 std::move(myPtr) 将 myPtr 的所有权转移给 processUniquePtr 函数。
- 在 processUniquePtr 函数中,打印出智能指针指向的值。
- 在所有权转移后,myPtr 被设置为 nullptr,因为它不再拥有资源。
#include <iostream>
#include <memory> // for std::shared_ptr and std::make_sharedvoid processSharedPtr(std::shared_ptr<int> ptr) {std::cout << "Processing shared pointer with value: " << *ptr << std::endl;std::cout << "Shared pointer use count: " << ptr.use_count() << std::endl;
}int main() {std::shared_ptr<int> myPtr = std::make_shared<int>(42);std::cout << "Initial use count: " << myPtr.use_count() << std::endl;// 使用 std::move 转移 myPtr 的所有权到 processSharedPtrprocessSharedPtr(std::move(myPtr));// myPtr 此时仍然是有效的,但 use_count 应该减少if (myPtr == nullptr) {std::cout << "myPtr is now nullptr after move" << std::endl;} else {std::cout << "myPtr is still valid after move, use count: " << myPtr.use_count() << std::endl;}return 0;
}
在这个例子中:
- 创建了一个 std::shared_ptr 类型的智能指针 myPtr,并使用 std::make_shared 进行初始化,指向一个值为 42 的整数。
- 打印初始的引用计数。
- 使用 std::move(myPtr) 将 myPtr 的所有权转移给 processSharedPtr 函数。
- 在 processSharedPtr 函数中,打印出智能指针指向的值和当前的引用计数。
- 在所有权转移后,myPtr 仍然有效,但引用计数应减少。
总结:
- std::move 对于智能指针(尤其是 std::unique_ptr)非常有用,可以转移所有权而不是复制资源。
- 对于 std::shared_ptr,使用 std::move 可以减少引用计数操作的开销,但共享资源的所有权仍然被多个智能指针共享。
参考
- 【C++11】自己封装RAII类,有哪些坑点?带你了解移动语义的真相
- move