【C++11】自己封装RAII类,有哪些坑点?带你了解移动语义的真相

文章目录

  • 一、持有资源的类定义移动构造函数的要点
    • 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/39429.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Wireshark - tshark支持iptables提供数据包

tshark现在的数据包获取方式有两种&#xff0c;分别是读文件、网口监听&#xff08;af-packet原始套接字&#xff09;。两种方式在包获取上&#xff0c;都是通过读文件的形式&#xff1b;存在文件io操作&#xff0c;在专门处理大流量的情境下&#xff0c; 我们复用wireshark去做…

Windows编程上

Windows编程[上] 一、Windows API1.控制台大小设置1.1 GetStdHandle1.2 SetConsoleWindowInfo1.3 SetConsoleScreenBufferSize1.4 SetConsoleTitle1.5 封装为Innks 2.控制台字体设置以及光标调整2.1 GetConsoleCursorInfo2.2 SetConsoleCursorPosition2.3 GetCurrentConsoleFon…

python如何输出list

直接输出list_a中的元素三种方法&#xff1a; list_a [1,2,3,313,1] 第一种 for i in range(len(list_a)):print(list_a[i]) 1 2 3 313 1 第二种 for i in list_a:print(i) 1 2 3 313 1 第三种&#xff0c;使用enumerate输出list_a方法&#xff1a; for i&#xff0c;j in enum…

Redis的使用(二)redis的命令总结

1.概述 这一小节&#xff0c;我们主要来研究一下redis的五大类型的基本使用&#xff0c;数据类型如下&#xff1a; redis我们接下来看一看这八种类型的基本使用。我们可以在redis的官网查询这些命令:Commands | Docs,同时我们也可以用help 数据类型查看命令的帮助文档。 2. 常…

数据结构 - C/C++ - 串

字符处理 C 特性 C语言中字符串存储在字符数组中&#xff0c;以空字符\0结束。 字符串常量&#xff0c;const char* str "Hello"&#xff0c;存储在只读的数据段中。 布局 字符串在内存中是字符连续存储的集合&#xff0c;最后一个字符为空字符(ASCII值为0)&…

opencascade AIS_InteractiveContext源码学习7 debug visualization

AIS_InteractiveContext 前言 交互上下文&#xff08;Interactive Context&#xff09;允许您在一个或多个视图器中管理交互对象的图形行为和选择。类方法使这一操作非常透明。需要记住的是&#xff0c;对于已经被交互上下文识别的交互对象&#xff0c;必须使用上下文方法进行…

【问题已解决】Vue管理后台,点击登录按钮,会发起两次网络请求(竟然是vscode Compile Hero编译插件导致的)

问题 VueElement UI 做的管理后台&#xff0c;点击登录按钮&#xff0c;发现 接口会连续掉两次&#xff0c;发起两次网络请求&#xff0c;但其他接口都是正常调用的&#xff0c;没有这个问题&#xff0c;并且登录按钮也加了loading&#xff0c;防止重复点击&#xff0c;于是开…

搜索引擎常用语法

引号 (" "): 用双引号将词组括起来&#xff0c;搜索引擎将返回包含完全相同短语的结果。 示例&#xff1a;"人工智能发展趋势" 减号 (-): 在关键词前加上减号可以排除包含特定词语的结果。 示例&#xff1a;人工智能 -机器学习&#xff08;排除包含 “机器…

朴素贝叶斯解密:sklearn中的分类器工作原理

&#x1f4da; 朴素贝叶斯解密&#xff1a;sklearn中的分类器工作原理 在机器学习领域&#xff0c;朴素贝叶斯分类器因其简单、高效而广受欢迎。特别是在处理大量特征数据时&#xff0c;朴素贝叶斯表现出了卓越的性能。scikit-learn&#xff08;简称sklearn&#xff09;是Pyth…

JavaMySQL 学习(基础)

目录 Java CMD Java发展 计算机存储规则 Java学习 switch新用法&#xff08;可以当做if来使用&#xff09; 数组定义 随机数 Java内存分配 MySQL MySQL概述 启动和停止 客户端连接 数据模型 关系型数据库 SQL SQL通用语法 SQL分类 DDL--数据定义语言 数据库…

浏览器开发者工具辅助爬虫开发

文章目录 浏览器开发者工具辅助爬虫开发打开开发者工具使用Network面板分析请求数据示例步骤&#xff1a; 使用Elements面板查看和修改DOM结构示例步骤&#xff1a; 使用Console面板调试JavaScript代码示例步骤&#xff1a;示例代码&#xff1a;1. 输出日志信息2. 输出对象信息…

Vue 与 React 区别

Vue.js和React是现代Web开发中两种非常流行的前端框架&#xff0c;两者在**核心概念、组件以及生态系统扩展性**等方面存在区别。具体分析如下&#xff1a; 1. **核心概念** - **Vue**&#xff1a;Vue是一个渐进式JavaScript框架&#xff0c;它致力于视图层&#xff0c;易于上手…

左值右值, 左值引用右值引用,完美转发

一. 左值和右值 左值: 可以取地址的对象 右值: 不可以取地址的对象 double x1.0, y 2.0; 1; // 字面量, 不可取地址, 是右值 x y; // 表达式返回值, 不可取地址, 是右值 max(x, y); // 传值返回函数的返回值 (非引用返回)总结就是: 根据是否可以取地址来区分是左值还…

线程池666666

1. 作用 线程池内部维护了多个工作线程&#xff0c;每个工作线程都会去任务队列中拿取任务并执行&#xff0c;当执行完一个任务后不是马上销毁&#xff0c;而是继续保留执行其它任务。显然&#xff0c;线程池提高了多线程的复用率&#xff0c;减少了创建和销毁线程的时间。 2…

git修改已提交的commit注释

在Git中修改已经提交的commit注释通常有以下几种情况和相应的方法&#xff1a; 1. 修改最后一次提交的注释&#xff08;快速修正&#xff09; 如果你想要修改的是最后一次提交的注释&#xff0c;可以使用 --amend 选项&#xff1a; git commit --amend这个命令会将你的暂存区…

基于深度学习的光度检测

基于深度学习的光度检测&#xff08;Photometric Detection&#xff09;涉及从图像中检测和分析光照信息&#xff0c;用于多种应用&#xff0c;如场景理解、照明调节、增强现实&#xff08;AR&#xff09;、图像增强等。以下是关于这一领域的系统介绍&#xff1a; 1. 任务和目…

JAVA基础教程DAY1-类与方法及形参实参

首先经过C语言的学习&#xff0c;我们已经学会了基本的编程方法&#xff0c;我们知道C语言是面向过程的编程语言&#xff0c;而JAVA是面向对象的编程语言&#xff0c;所以接下来我们通过对比和举例来进行JAVA语言的学习 首先我们来讲类的概念 类&#xff1a;类是一个模板&…

Ubuntu开通5005端口 记录

Ubuntu版本&#xff1a;20.04 使用systemctl status firewalld查看防火墙状态&#xff0c;报错Unit firewalld.service could not be found 报错的原因是没有安装firewall&#xff0c;安装命令为sudo apt install firewalld&#xff0c;然后进行安装 安装完成后输入systemctl…

vscode jupyter选择Python环境时找不到我安装的Python

在一些情况下&#xff0c;我们需要自己安装一个Python&#xff0c;在选择内核是可能找不到指定的Python版本&#xff0c; 再次打开内核选择页面就能看到Python环境了 注意先到指定环境下安装依赖包&#xff1a; ./python3 pip install ipykernel notebook jupyter

人工智能-NLP简单知识汇总01

人工智能-NLP简单知识汇总01 1.1自然语言处理的基本概念 自然语言处理难点&#xff1a; 语音歧义句子切分歧义词义歧义结构歧义代指歧义省略歧义语用歧义 总而言之&#xff1a;&#xff01;&#xff01;语言无处不歧义 1.2自然语言处理的基本范式 1.2.1基于规则的方法 通…