C++的移动语义和完美转发

参考《现代C++语言核心特性解析》

移动语义

C++11新特性的std::move()用于将一个左值转换为右值引用。它并不是实际移动或复制数据,而是通过将一个左值强制转换为一个右值引用来实现对对象的转移。这个特性在C++11中引入,用于优化对象移动操作的效率。

我们知道,右值引用只能引用右值,如果尝试绑定左值就会编译错误。

int i = 0;
int &&k = i;	// 编译错误

在C++11标准中可以在不创建临时值的情况下显式地将左值通过static_cast转换为将亡值,通过值类别的内容我们知道将亡值属于右值,所以可以被右值引用绑定。值得注意的是,由于转换的并不是右值,因此它依然有着和转换之前相同的生命周期和内存地址,例如:

int i = 0;
int &&k = static_cast<int&&>(i);

既然这个转换既不改变生命周期,也不改变内存地址,那它存在的意义是什么?实际上它最大的作用是让左值使用移动语义。

举例:

#include <iostream>

class BigMemoryPool
{
public:
    static const int PoolSize = 4096;

    BigMemoryPool() : pool_(new char[PoolSize])
    {
        std::cout << "普通构造函数" << std::endl;
    }

    ~BigMemoryPool()
    {
        if (pool_ != nullptr)
        {
            delete[] pool_;
        }
    }

    BigMemoryPool(BigMemoryPool &&other) : pool_(new char[PoolSize])
    {
        std::cout << "移动构造函数" << std::endl;
        pool_ = other.pool_;
        other.pool_ = nullptr;
    }

    BigMemoryPool(const BigMemoryPool &other) : pool_(new char[PoolSize])
    {
        std::cout << "拷贝构造函数" << std::endl;
        memcpy(pool_, other.pool_, PoolSize);
    }

private:
    char *pool_;
};

BigMemoryPool get_pool(const BigMemoryPool &pool)
{
    return pool;
}

BigMemoryPool make_pool()
{
    BigMemoryPool pool;
    return get_pool(pool);
}

int main()
{
    BigMemoryPool my_pool1;
    BigMemoryPool my_pool2 = my_pool1;
    BigMemoryPool my_pool3 = static_cast<BigMemoryPool &&>(my_pool1);

    return 0;
}

在这段代码中,my_pool1是一个BigMemoryPool类型的对象,也是一个左值,所以用它去构造my_pool2的时候调用的是复制构造函数。为了让编译器调用移动构造函数构造my_pool3,这里使用了static_cast<BigMemoryPool &&>(my_pool1)将my_pool1强制转换为右值(也是将亡值,为了叙述思路的连贯性后面不再强调)。由于调用了移动构造函数,my_pool1失去了自己的内存数据,后面的代码也不能对my_pool1进行操作了。

结果输出:

PS C:\Users\zh'n\Desktop\新建文件夹> g++ -std=c++11 -fno-elide-constructors main.cpp -o main
PS C:\Users\zh'n\Desktop\新建文件夹> ./main
普通构造函数
拷贝构造函数
移动构造函数

但是这个示例中把my_pool1这个左值转换成my_pool3这个左值似乎没有什么意义,而且程序员如果再次去访问my_pool1还会引发未定义行为。

正确的使用场景是在一个右值被转换为左值后需要再次转换为右值,最典型的例子是一个右值作为实参传递到函数中。我们在讨论左值和右值的时候曾经提到过,无论一个函数的实参是左值还是右值,其形参都是一个左值,即使这个形参看上去是一个右值引用,例如:

void move_pool(BigMemoryPool &&pool)
{
  std::cout << "call move_pool" << std::endl;
  BigMemoryPool my_pool(pool);
}

int main()
{
  move_pool(make_pool());
}

结果输出:

PS C:\Users\zh'n\Desktop\新建文件夹> g++ -std=c++11 -fno-elide-constructors main.cpp -o main
PS C:\Users\zh'n\Desktop\新建文件夹> ./main
普通构造函数
拷贝构造函数
移动构造函数
call move_pool
拷贝构造函数

代码中,make_pool()返回的是一个临时对象,也是一个右值,move_pool的参数是一个右值引用,但是在使用形参pool去构造my_pool时调用的是拷贝构造函数。如果我们想调用移动构造函数的话,需要把形参pool强制转换为右值。

void move_pool(BigMemoryPool &&pool)
{
    std::cout << "call move_pool" << std::endl;
    BigMemoryPool my_pool = static_cast<BigMemoryPool &&>(pool); // 1
}

结果输出:

PS C:\Users\zh'n\Desktop\新建文件夹> g++ -std=c++11 -fno-elide-constructors main.cpp -o main
PS C:\Users\zh'n\Desktop\新建文件夹> ./main
普通构造函数
拷贝构造函数
移动构造函数
call move_pool
移动构造函数

请注意,在这个场景下强制转换为右值就没有任何问题了,因为move_pool函数的实参是make_pool返回的临时对象,当函数调用结束后临时对象就会被销毁,所以转移其内存数据不会存在任何问题。

在C++11的标准库中还提供了一个函数模板std::move帮助我们将左值转换为右值,这个函数内部也是用static_cast做类型转换。只不过由于它是使用模板实现的函数,因此会根据传参类型自动推导返回类型,省去了指定转换类型的代码。另一方面从移动语义上来说,使用std::move函数的描述更加准确。所以建议读者使用std::move将左值转换为右值而非自己使用static_cast转换,例如:

void move_pool(BigMemoryPool &&pool)
{
    std::cout << "call move_pool" << std::endl;
    BigMemoryPool my_pool(std::move(pool)); // 1
}

总结:

std::move()内部是用static_cast做类型转换,只不过它是使用模板实现的函数,因此会根据传参类型自动推导返回值类型,省去了指定类型的代码。如果使用std::move()将一个左值转换为右值并赋值给其他对象后,这个对象就会被销毁,所以在函数调用过程中,创建N个对象实际上只是把第一个对象的内存不断的转移,类似层层递归。 这样做的好处就是省去了创建对象的开销,并且在对象副本庞大的情况下节省了大量时间。

完美转发

在了解完美转发之前,先了解一下什么是万能引用和引用折叠。

我们知道常量左值引用可以引用左值,也可以引用右值,是一个几乎的万能引用,但是因为它的常量性导致使用受限制。

在C++11中有一个“万能引用”,例如:

void foo(int &&i){} // 右值引用

template<class T>
void bar(T &&t){} // 万能引用

int get_val(){return 5;}
int &&x = get_val(); // 右值引用
auto &&x = get_val(); // 万能引用

我们可以发现,只要是自动类型推导的引用就是万能引用。在这个推导过程中,源对象是左值,那就推导为左值引用,源对象是右值,那就推导为右值引用。

万能引用能如此灵活地引用对象,实际上是因为在C++11中添加了一套引用叠加推导的规则——引用折叠。在这套规则中规定了在不同的引用类型互相作用的情况下应该如何推导出最终类型。

在这里插入图片描述
举例说明:

int i = 42;
const int j = 11;
bar(i);
bar(j);
bar(get_val());

auto &&x = i;
auto &&y = j;
auto &&z = get_val();

在bar(i);中i是一个左值,所以T的推导类型结果是int&,根据引用折叠规则int& &&的最终推导类型为int&,于是bar函数的形参是一个左值引用。而在bar(get_val());中get_val返回的是一个右值,所以T的推导类型为非引用类型int,于是最终的推导类型是int&&,bar函数的形参成为一个右值引用。

完美转发的用途
看一个常规的转发函数模板

#include <iostream>
#include <string>
#include <typeinfo>

template<class T>
void show_type(T t)
{
  std::cout << typeid(t).name() << std::endl;
}

template<class T>
void normal_forwarding(T t)
{
  show_type(t);
}

int main()
{
  std::string s = "hello world";
  normal_forwarding(s);
}

// 输出:Ss

normal_forwarding函数可以完成字符串的转发任务,但是它的效率很慢。首先它的参数是值传递,那么在转发过程中就会发生一次临时对象的复制。其中一个解决方法就是把void normal_forwarding(T t)换成void normal_forwarding(T& t),通过引用传递,但这是一个左值引用,如果参数是一个右值就会编译失败。

std::string get_string()
{
  return "hi world";
}

normal_forwarding(get_string());    // 编译失败

但是常量左值可以引用右值,可以解决这个问题,但引来的新问题是常量左值引用具有常量性,使得对象不可以被修改。

所以万能引用的诞生解决了这个问题。

对于万能引用来说,如果实参是一个左值,那么形参会被推导为左值引用、如果实参是一个右值,那么形参会被推导为右值引用。

#include <iostream>
#include <string>

template<class T>
void show_type(T t)
{
  std::cout << typeid(t).name() << std::endl;
}

template<class T>
void perfect_forwarding(T &&t)	// 万能引用
{
  show_type(static_cast<T&&>(t));
}

std::string get_string()
{
  return "hi world";
}

int main()
{
  std::string s = "hello world";
  perfect_forwarding(s);
  perfect_forwarding(get_string());
}

和移动语义的情况一样,显式使用static_cast类型转换进行转发不是一个便捷的方法。在C++11的标准库中提供了一个std::forward函数模板,在函数内部也是使用static_cast进行类型转换,只不过使用std::forward转发语义会表达得更加清晰,std::forward函数模板的使用方法也很简单:

template<class T>
void perfect_forwarding(T &&t)
{
  show_type(std::forward<T>(t));
}

请注意std::move和std::forward的区别,其中std::move一定会将实参转换为一个右值引用,并且使用std::move不需要指定模板实参,模板实参是由函数调用推导出来的。而std::forward会根据左值和右值的实际情况进行转发,在使用的时候需要指定模板实参。

完整示例:

#include <iostream>
#include <string>
#include <typeinfo>

template <class T>
void show_type(T t)
{
    std::cout << typeid(t).name() << std::endl;
}

template <class T>
void perfect_forwarding(T &&t)
{
    show_type(std::forward<T>(t));
}

int main()
{
    std::string s = "hello world";
    perfect_forwarding(s);	// 实参是左值
    perfect_forwarding(1.0); // 实参是右值
}

// 输出
// Ss
// d

总结

完美转发允许将函数的参数(包括左值和右值)转发给其他函数,同时保持原始参数的值不变,这样可以实现高效的函数调用。

#include <iostream>
#include <utility>

template <typename T>
void process(T &i)
{
    std::cout << "L-value: " << i << std::endl;
}

template <typename T>
void process(T &&i)
{
    std::cout << "R-value: " << i << std::endl;
}

template <typename T>
void forwarder(T &&t)
{
    process(std::forward<T>(t));
}

int main()
{
    int a = 42;
    forwarder(a); // L-value: 42

    forwarder(7.1); // R-value: 7

    return 0;
}

// 输出
// L-value: 42
// R-value: 7.1

在上面的示例中,forwarder函数使用了完美转发,它接受一个泛型类型的参数T&& t,并将参数t转发给process函数。通过使用std::forward(t),可以将原始参数的值类别(左值或右值)传递给process函数,从而调用合适的重载函数。

通过使用完美转发,可以更好地处理函数参数的转发,避免不必要的拷贝,提高代码的性能和效率。请注意,完美转发需要注意避免悬垂引用和引用折叠等问题,在实际使用中需要谨慎处理。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Labelme2Yolo labelme格式的json标注转yolo格式txt

该工作适用于目标检测工作。 由于labelme标注出的文件是如下图的单个json文件格式&#xff0c;不符合yolo的训练格式&#xff0c;需要转格式。 观察发现labelme标注的json文件中有imageData&#xff0c;还挺大的&#xff0c;查阅后得知是base64后的图片数据&#xff0c;也就是…

网易有道强力开源中英双语语音克隆

项目地址&#xff08;基于PromptTTS&#xff09;&#xff1a; https://github.com/netease-youdao/EmotiVoice EmotiVoice Docker镜像 尝试EmotiVoice最简单的方法是运行docker镜像。你需要一台带有NVidia GPU的机器。先按照Linux和Windows WSL2平台的说明安装NVidia容器工具…

Linux——基本指令(二)

​ 个人主页&#xff1a;日刷百题 系列专栏&#xff1a;〖C语言小游戏〗〖Linux〗〖数据结构〗 〖C语言〗 &#x1f30e;欢迎各位→点赞&#x1f44d;收藏⭐️留言&#x1f4dd; ​ ​ 写在前面&#xff1a; 紧接上一章&#xff0c;我们在理解接下来的命令之前&#xff0c…

收款码在线生成系统源码/开源layui前端框架/附多套前端UI模板/三合一收款码生成系统源码

源码简介&#xff1a; 收款码在线生成系统源码&#xff0c;它是采用开源layui前端框架&#xff0c;并且它附多套前端UI模板&#xff0c;作为三合一收款码生成系统源码&#xff0c;界面简洁大方。 Layui前端的多合一收款码在线生成系统源码&#xff0c;附带了多套精美的前端UI…

mac 环境下 goframe安装GF开发工具 gf-cli(安装包方式安装)

mac 环境下 goframe安装GF开发工具 gf-cli&#xff08;安装包方式安装&#xff09; 安装包网址 链接: link 终端输入命令进行安装 ./gf_darwin_amd64 但是产生如下报错&#xff0c;无法安装 使用一下命令给安装权限 chmod 0777 gf_darwin_amd64 && ./gf_darwin_a…

巧妙的使用WPF中的资源

其实&#xff0c;在wpf中&#xff0c;最核心的就是xaml&#xff0c;因为只有xaml&#xff0c;才能体现出用的是wpf&#xff0c;而不是普通的cs文件&#xff0c;cs文件在winform中等等程序都可以使用的&#xff0c;唯独xaml才是wpf中最重要的&#xff0c;最精华的东西&#xff0…

【JVM入门到实战】(三) 查看字节码文件的工具

一、 javap -v命令 javap是JDK自带的反编译工具&#xff0c;可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。直接输入javap查看所有参数。输入javap -v 字节码文件名称 查看具体的字节码信息。&#xff08;如果jar包需要先使用 jar –xvf 命令解压&a…

c语言 词法分析器 《编译原理》课程设计

设计、编制并调试一个词法分析程序&#xff0c;加深对词法分析原理的理解。 针对表达各类词语的一组正规表达式&#xff0c;设计一个确定化的最简的有限自动机&#xff0c;对输入的符号串进行单词划分及词类识别。 要求词法分析器的输入是字符串&#xff0c;输出是源程序中各…

9:00面试,9:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到12月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40…

生产派工自动化:MES系统的关键作用

随着制造业的数字化转型和智能化发展&#xff0c;生产派工自动化成为了提高生产效率、降低成本&#xff0c;并实现优质产品生产的关键要素之一。制造执行系统&#xff08;MES&#xff09;在派工自动化中发挥着重要作用&#xff0c;通过实时数据采集和智能调度&#xff0c;优化生…

Baumer工业相机堡盟工业相机如何通过BGAPISDK获取相机的各种信息如SN/ID等等(C#)

Baumer工业相机堡盟工业相机如何通过BGAPISDK获取相机的各种信息如SN/ID等等&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机通过SDK获取相关生产信息的技术背景通过SDK获取相机信息的代码分析获取Baumer工业相机相关信息Baumer工业相机相关参数信息获取的测试 Baume…

回归预测 | MATLAB实现IWOA-LSTM改进鲸鱼算法算法优化长短期记忆神经网络的数据回归预测(多指标,多图)

回归预测 | MATLAB实现IWOA-LSTM改进鲸鱼算法算法优化长短期记忆神经网络的数据回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现IWOA-LSTM改进鲸鱼算法算法优化长短期记忆神经网络的数据回归预测&#xff08;多指标&#xff0c;多图&#…

使用pdf2docx轻松将PDF转换成docx

目录 一、什么是pdf2docx&#xff1f; 二、为什么选择pdf2docx&#xff1f; 三、如何使用pdf2docx&#xff1f; 四、常见问题和解决方案 总结 随着数字化时代的到来&#xff0c;PDF和docx格式已经成为我们日常工作中最常用的文档格式之一。然而&#xff0c;有时我们需要将…

PDF控件Spire.PDF for .NET【转换】演示:将PDF彩色图像转换为灰度(黑白)

将包含彩色图像的 PDF 转换为灰度图像可以帮助您减小文件大小&#xff0c;并以更实惠的模式打印 PDF&#xff0c;而无需消耗彩色墨水。在本文中&#xff0c;您将了解如何使用Spire.PDF for .NET在 C# 和 VB.NET 中以编程方式实现转换。 Spire.Doc 是一款专门对 Word 文档进行操…

HarmonyOS 的应用开发语言:ArkTS

本心、输入输出、结果 文章目录 HarmonyOS 的应用开发语言&#xff1a;ArkTS前言ArkTS 产生背景ArkTS 语言特点ArkTS 基本语法ArkTS 声明式 UIArkTS 状态管理ArkTS 渲染控制 ArkTS 轻量化并发机制ArkTS 相关文档花有重开日&#xff0c;人无再少年实践是检验真理的唯一标准 Harm…

MIT18.06线性代数 笔记1

文章目录 方程组的几何解释矩阵消元乘法和逆矩阵A的LU分解转置-置换-向量空间R列空间和零空间求解Ax0主变量 特解求解Axb可解性和解的结构线性相关性、基、维数四个基本子空间矩阵空间、秩1矩阵和小世界图图和网络复习一 方程组的几何解释 线性组合&#xff1a; 找到合适的x和…

FL Studio Producer Edition 21.2.2.3914中文汉化破解版新功能介绍及下载安装教程

FL Studio Producer Edition 21.2.2.3914中文汉化破解版 也就是 Image-Line 出品的一款功能强大的编曲软件&#xff0c;全名 Fruity Loops Studio 简称“FL Studio”今天突然的发现我们经常使用的水果音乐制作软件 FL STUDIO 居然从FL STUDIO 21.1.1 一下子跨越了版本号到了FL …

Orange Comet利用Sui Kiosk进行游戏道具和知识产权保护

Orange Comet与AMC合作开发基于《行尸走肉》系列的NFT和游戏&#xff0c;首要关注的问题就是保护AMC的知识产权。利用Sui的Kiosk原语不仅让Orange Comet向AMC保证其资产安全&#xff0c;而且为即将推出的《行尸走肉大陆》游戏打开了无限的可能性。 Kiosk是Sui上的一个原语&…

四六级高频词组5

词组 151. be content with&#xff08;be satisfied with&#xff09; 满足于 be content to do sth. 愿意做某事 152. contrary to &#xff08;in opposition to&#xff09; 与…相反 153. on the contrary 相反 154. contrast…with 把…与…相对&#xff08;对照&am…

『npm』一条命令快速配置npm淘宝国内镜像

&#x1f4e3;读完这篇文章里你能收获到 一条命令快速切换至淘宝镜像恢复官方镜像 文章目录 一、设置淘宝镜像源二、恢复官方镜像源三、查看当前使用的镜像 一、设置淘宝镜像源 npm config set registry https://registry.npm.taobao.org服务器建议全局设置 sudo npm config…