Modern Effective C++ 条款二十九三十:移动语义和完美转发失败的情况

条款二十九:假定移动操作不存在,成本高,未被使用

移动语义可以说是C++11最主要的特性。"移动容器和拷贝指针一样开销小","拷贝临时对象现在如此高效,“写代码避免这种情况简直就是过早优化"。很多开发者认为通过采用移动语义可以极大地优化代码效率,甚至有时会听到“移动容器与拷贝指针一样开销小”这样的说法,以及建议不必过分担心临时对象的复制问题,因为移动语义已经让这个过程变得非常高效。然而移动语义有失败的情况。

移动语义允许通过移动操作来提高程序性能。然而,并非所有类型都支持移动操作,且即使支持,其带来的性能提升也未必如预期般显著。本文将讨论C++11中移动语义的局限性及其在不同类型中的表现。

1. 容器差异

案例1:std::array

并非所有容器都能从移动语义中获得相同程度的好处。例如std::vector等基于堆分配内存的容器可以通过简单地转移指针实现高效移动。std::array直接存储元素而非指向动态分配内存的指针,因此其移动操作涉及每个元素的单独处理,开销为线性时间复杂度。

考虑一下std::array(C++11中的新容器)>。std::array本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器,本身只保存了指向堆内存中容器内容的指针(实现更复杂,基本逻辑是这样)。这个指针的存在使得在常数时间移动整个容器成为可能,只需要从源容器拷贝保存指向容器内容的指针到目标容器,然后将源指针置为空指针就可以了:

std::vector<Widget> vw1;
//把数据存进vw1.
//把vw1移动到vw2.
//以常数时间运行.只有vw1和vw2中的指针被改变
auto vw2 = std::move(vw1);

std::array没有这种指针实现,数据就保存在std::array对象中:

std::array<Widget, 10000> aw1;
//把数据存进aw1
//把aw1移动到aw2。以线性时间运行
//aw1中所有元素被移动到aw2
auto aw2 = std::move(aw1);

aw1中的元素被移动到了aw2中。假定Widget类的移动操作比复制操作快,移动Widgetstd::array就比复制要快。所以std::array确实支持移动操作。但是使用std::array的移动操作还是复制操作都将花费线性时间的开销,因为每个容器中的元素终归需要拷贝或移动一次,这与“移动一个容器就像操作几个指针一样方便”的含义相去甚远。

案例2:std::string

尽管std::string提供了常数时间复杂度的移动操作与线性时间复杂度的复制操作,但小字符串优化(SSO)使得对于短字符串来说,移动并不比复制更高效。SSO允许短字符串直接存储在std::string对象缓冲区,避免了额外的堆内存分配。在这种情况下,移动这样的字符串不会比复制更快。S大量证据表明,短字符串是大量应用使用的习惯。使用内存缓冲区存储而不分配堆内存空间,是为了更好的效率。

2. 异常安全与移动操作

异常安全性:C++标准库中的某些容器为了提供强大的异常安全保障,要求移动操作必须是noexcept。如果一个类提供的移动操作没有声明为noexcept,即使它实际上更高效,编译器也可能选择使用复制操作以确保代码的异常安全性。这限制了移动语义在这些情况下的应用。

情况分析

(1)没有移动操作:如果要移动的对象不支持移动操作,则移动表达式会退化为复制操作。

(2)移动不比复制快:即使存在移动操作,如果移动的成本不低于复制,那么移动可能并不会带来性能上的提升。

(3)移动不可用:在需要保证移动操作不会抛出异常的情况下,若移动操作未声明为noexcept,则编译器将不得不使用复制操作。

(4)源对象是左值:通常只有右值可以作为移动操作的来源;左值作为来源时,除非特别设计,否则一般采用复制而非移动。

通用编程中的考虑

编写泛型代码或模板时,由于无法预知具体类型是否支持高效的移动操作,应谨慎地依赖于复制操作。这类不稳定的代码经常变更,导致类型特性变化,也应采取保守策略。当对所使用的类型有充分了解,并且该类型确实支持快速移动操作时,可以在合适的上下文中利用这一点来替换复制操作,从而提高性能。

条款三十:熟悉完美转发失败的情况

C++11的完美转发是非常好用,但是只有当你愿意忽略一些误差情况(完美转发失败的情况),这个条款就是使你熟悉这些情形。

完美转发(perfect forwarding)意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是const还是volatile。结合到我们会处理引用形参,意味着将使用通用引用,因为通用引用形参被传入实参时才确定是左值还是右值。

假定有一些函数f,编写一个转发给它的函数(事实上是一个函数模板)。我们需要的核心看起来像是这样:

template<typename T>
void fwd(T&& param)//接受任何实参{f(std::forward<T>(param));  //转发给f
}

转发函数是通用的。例如fwd模板,接受任何类型的实参,并转发得到的任何东西。这种通用性的逻辑扩展是,转发函数不仅是模板,而且是可变模板,因此可以接受任何数量的实参。fwd的可变形式如下:

template<typename... Ts>
void fwd(Ts&&... params)//接受任何实参{f(std::forward<Ts>(params)...); //转发给f
}

这种形式你会在容器emplace functions中(item42)和 智能指针的工厂函数std::make_uniquestd::make_shared中(item21)看到,当然还有其他一些地方。

给定我们的目标函数f和转发函数fwd,如果f使用某特定实参会执行某个操作,但是fwd使用相同的实参会执行不同的操作,完美转发就会失败。导致这种失败的实参种类有很多。知道它们是什么以及如何解决它们很重要,因此让我们来看看无法做到完美转发的实参类型。

花括号初始化器

假定f这样声明

void f(conststd::vector<int>& v);

用花括号初始化调用f通过编译

f({ 1, 2, 3 });  //可以,“{1, 2, 3}”隐式转换为std::vector<int>

但是传递相同的列表初始化给fwd不能编译

fwd({ 1, 2, 3 }); //错误!不能编译

这是完美转发失效的一种情况。当通过调用函数模板fwd间接调用f时,编译器不再把调用地传入给fwd的实参和f的声明中形参类型进行比较。而是推导传入给fwd的实参类型,然后比较推导后的实参类型和f的形参声明类型。当下面情况任何一个发生时,完美转发就会失败:

编译器不能推导出fwd的一个或者多个形参类型。 这种情况下代码无法编译。

编译器推导“错”了fwd的一个或者多个形参类型。

"错误"可能意味着fwd的实例将无法使用推导出的类型进行编译,但是也可能意味着使用fwd的推导类型调用f,与用传给fwd的实参直接调用f表现出不一致的行为。

这种不同行为的原因可能是因为f是个重载函数的名字,并且由于是“不正确的”类型推导,在fwd内部调用的f重载和直接调用的f重载不一样。

在上面的fwd({ 1, 2, 3 })例子中,将花括号初始化传递给未声明为std::initializer_list的函数模板形参。着编译器不准在对fwd的调用中推导表达式{ 1, 2, 3 }的类型,因为fwd的形参没有声明为std::initializer_list。对于fwd形参的推导类型被阻止,编译器只能拒绝该调用。

item2说明了使用花括号初始化的auto的变量的类型推导是成功的。这种变量被视为std::initializer_list对象,在转发函数应推导出类型为std::initializer_list的情况,这提供了一种简单的解决方法:使用auto声明一个局部变量,然后将局部变量传进转发函数:

auto il = { 1, 2, 3 };  //il的类型被推导为std::initializer_list<int>
fwd(il);                //可以,完美转发il给f

(这个地方没看懂)

0或者NULL作为空指针

item8说明当试图传递0或者NULL作为空指针给模板时,类型推导会出错,会把传来的实参推导为一个整型类型(典型情况为int)而不是指针类型。

结果就是不管是0还是NULL都不能作为空指针被完美转发。解决方法非常简单,传一个nullptr而不是0或者NULL(参考item8)。

仅有声明的整型static const数据成员

通常无需在类中定义整型static const数据成员;声明就可以了。这是因为编译器会对此类成员实行常量传播(const propagation),因此消除了保留内存的需要。

声明与定义的区别

告诉编译器某个实体(变量,函数,类等)的名称和类型,不为其分配存储空间。不仅告诉编译器实体的名称和类型,还为其分配存储空间,并可能提供初始化值或实现。

static const 整型数据成员的特殊情况。

对于static const整型数据成员,可以在类内部进行初始化,但这实际上是声明的一部分。编译器会将这个常量的值传播到所有使用它的地方,从而不需要为它分配实际的存储空间。例如:

class Widget {
public:static const std::size_t MinVals = 28;  // 声明并初始化
};

MinVals被声明为static const整型数据成员,并且在类内部进行了初始化。编译器会在所有使用Widget::MinVals的地方直接替换为28,因此不需要为MinVals分配内存。

为什么这仍然是声明?

编译器优化:编译器会对static const整型数据成员进行常量传播,这意味着它可以直接将值28插入到所有使用MinVals的地方,而不需要实际的存储空间。由于不需要实际的存储空间,MinVals不会在对象文件中生成符号,因此不会产生外部链接。

如果需要通过引用或指针传递MinVals,或者需要获取其地址,那么必须提供一个定义。

// 在Widget的.cpp文件中
const std::size_t Widget::MinVals;  // 定义

引用和指针需要实际的内存地址来指向,而不仅仅是值。如果没有定义,编译器将无法找到实际的内存地址,导致链接错误。

class Widget{public:static const std::size_t MinVals = 28; //声明并初始化
};
//使用
void printValue(std::size_t val) {std::cout << "Value: " << val << std::endl;
}template<typename T>
void fwd(T&& arg) {printValue(std::forward<T>(arg));
}int main() {std::vector<int> widgetData;widgetData.reserve(Widget::MinVals);//使用MinValsprintValue(Widget::MinVals);//可以,视为“printValue(28)”//下面的调用会导致链接错误,因为fwd需要实际的内存地址//fwd(Widget::MinVals);  // 错误!不应该链接return 0;
}

为了修复链接错误,需要在.cpp文件中提供定义:

// 在Widget的.cpp文件中
const std::size_t Widget::MinVals;  // 定义

尽管代码中没有使用MinVals的地址,但是fwd的形参是通用引用,引用在编译器生成的代码中,通常被视作指针。在程序的二进制底层代码中指针和引用是一样的。在这个水平上,引用只是可以自动解引用的指针。通过引用传递MinVals实际上与通过指针传递MinVals是一样的,因此,必须有内存使得指针可以指向。通过引用传递的整型static const数据成员,通常需要定义它们,这个要求可能会造成在不使用完美转发的代码成功的地方,使用等效的完美转发失败。

根据标准,通过引用传递MinVals要求有定义。但不是所有的实现都强制要求这一点。所以,取决于你的编译器和链接器,你可能发现你可以在未定义的情况使用完美转发,恭喜你,但是这不是那样做的理由。为了具有可移植性,只要给整型static const提供一个定义:

const std::size_t Widget::MinVals;  //在Widget的.cpp文件

注意定义中不要重复初始化(这个例子中就是赋值28)。但是不要忽略这个细节。如果你忘了,并且在两个地方都提供了初始化,编译器就会报错,提醒你只能初始化一次。

重载函数与模板名称在完美转发中的问题

假设有一个函数f,接受一个函数指针作为参数,并且这个函数指针的类型是int (*)(int)。可以通过传递一个符合条件的函数来定制f的行为。

void f(int (*pf)(int));  // pf = "process function"
// 或者使用更简单的非指针语法
void f(int pf(int));

重载函数的问题

如果有一个重载函数processVal

int processVal(int value);
int processVal(int value, int priority = 1);

可以直接将processVal传递给f,因为编译器可以根据f的参数类型推断出需要哪个版本的processVal:

f(processVal);  //可以,编译器选择正确的processVal版本

当使用一个完美转发函数fwd时,问题出现了。fwd是一个模板函数,没有具体的类型信息,因此编译器无法确定应该传递哪个版本的processVal。

template<typename T>
void fwd(T&& arg) {f(std::forward<T>(arg));
}
fwd(processVal);  // 错误!哪个processVal?

函数模板的问题

同样的问题也出现在函数模板上。假设有一个函数模板workOnVal:

template<typename T>
T workOnVal(T param) {// 处理值的模板
}

尝试将workOnVal传递给fwd也会失败,因为编译器不知道应该实例化哪个版本的workOnVal:

fwd(workOnVal);  // 错误!哪个workOnVal实例?

解决方法

为了使完美转发能够处理重载函数或函数模板,需要显式地指定要传递的具体函数或函数模板实例。这可以通过创建一个具体类型的函数指针来实现。(这个没看懂)

定义类型别名:

定义一个类型别名,表示所需的函数指针类型。

using ProcessFuncType = int (*)(int);  // 定义类型别名

创建函数指针:

使用该类型别名创建一个函数指针,并将其初始化为所需的重载函数或函数模板实例。

ProcessFuncType processValPtr = processVal;  // 指定所需的processVal签名

传递函数指针:

将创建的函数指针传递给fwd。

fwd(processValPtr);  // 可以

处理函数模板:

对于函数模板,可以使用static_cast来显式地实例化并传递。

fwd(static_cast<ProcessFuncType>(workOnVal));  // 也可以

#include <iostream>
// 目标函数
void f(int (*pf)(int)) {std::cout << "f called with: " << pf(42) << std::endl;
}
// 重载函数
int processVal(int value) {return value * 2;
}
int processVal(int value, int priority) {return value + priority;
}
// 函数模板
template<typename T>
T workOnVal(T param) {return param * 3;
}
// 完美转发函数
template<typename T>
void fwd(T&& arg) {f(std::forward<T>(arg));
}
int main() {// 定义类型别名using ProcessFuncType = int (*)(int);// 创建函数指针ProcessFuncType processValPtr = processVal;// 传递函数指针fwd(processValPtr);  // 正确// 传递函数模板实例fwd(static_cast<ProcessFuncType>(workOnVal<int>));  // 正确return 0;
}

位域与完美转发的问题

位域(bit-fields)是一种特殊的成员变量,用于节省内存空间。它们通常用来表示结构体或类中的小整数。然而,当涉及到完美转发时,位域会带来一些特殊的问题。位域可能只占用一个机器字的部分位,例如32位整型中的某些位。这些位无法直接寻址,因此不能通过指针或引用直接访问。C++标准明确禁止非const引用绑定到位域上,因为位域可能不是对齐的,且不支持直接寻址。

假设有一个IPv4头部结构体定义如下:

struct IPv4Header {std::uint32_t version:4,IHL:4,DSCP:6,ECN:2,totalLength:16;
//其他字段...
};

如果有一个函数f接收一个std::size_t类型的参数,并且希望使用IPv4Header对象的totalLength字段调用它,那么直接调用是可行的:

void f(std::size_t sz);  //要调用的函数
IPv4Header h;
// 填充h.totalLength
f(h.totalLength);  //可以正常工作

但是,如果你希望通过一个转发函数fwd来调用f,并且fwd的参数是引用类型,那么就会出现问题。

template<typename T>
void fwd(T&& arg) {f(std::forward<T>(arg));
}
IPv4Header h;
// 填充h.totalLength
fwd(h.totalLength);  // 错误!non-const引用不能绑定到位域

解决方法

为了使完美转发能够处理位域,可以采取以下几种方法:

(1)按值传递

将位域的值复制到一个新的变量中,然后将这个新变量传递给目标函数。

auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length);  // 正确,传递的是副本

(2)传const引用

使用const引用传递位域的值。根据C++标准,const引用实际上绑定到一个包含位域值的临时整型对象。

void f(const std::uint16_t& sz); //修改f的参数为const引用
template<typename T>
void fwd(T&& arg){f(std::forward<T>(arg));
}
IPv4Header h;
//填充h.totalLength
fwd(h.totalLength);  //正确,const引用绑定到临时对象

(3)显式创建临时对象

在调用转发函数之前,显式地创建一个临时对象,然后传递这个临时对象。

  auto length = static_cast<std::uint16_t>(h.totalLength);fwd(length);  // 正确,传递的是副本

请记住:

  • 当模板类型推导失败或者推导出错误类型,完美转发会失败。
  • 导致 完美转发失败的实参种类有 花括号初始化,作为 空指针的0 或者 NULL,仅有声明的整型static const数据成员,模板和重载函数的名字,位域。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

C++【模板】plus

目录 一、非类型模板参数 1.引入 2.使用 二、模板特化 1.函数模板特化 2.特化失效 3.类模板特化 应用 三、*带模板的分离编译 一、非类型模板参数 1.引入 我们使用宏对某个变量进行定值&#xff0c;如 #define N10 --->那么N在下面使用时始终为10&#xff0c;如果…

Leetcode 每日一题 290.单词规律

目录 一、问题分析 二、解题思路 三、代码实现 四、复杂度分析 五、总结 在编程的世界里&#xff0c;我们常常会遇到各种有趣的字符串匹配问题。今天要探讨的就是这样一个问题&#xff1a;给定一种规律 pattern 和一个字符串 s&#xff0c;判断 s 是否遵循与 pattern 相同…

浅谈FRTC8563M实时时钟芯片

FRTC8563M是NYFEA徕飞公司推出的一款实时时钟芯片和日历芯片&#xff0c;采用MSOP-8封装形式。它具有低功耗特性&#xff0c;适用于电池供电的便携式设备。该芯片提供年、月、日、星期、小时、分钟和秒的计时功能&#xff0c;并且具有闹钟功能。FRTC8563M通过I2C总线与微控制器…

HOC vs Render Props vs Hooks

相关问题 什么是 HOC / Render Props / Hooks为什么需要 HOC / Render Props / Hooks如何提高代码复用性Hooks 的实现原理Hooks 相比其他方案有什么优势 关键点 复用性HOC / Render Props / Hooks 三种写法都可以提高代码的复用性&#xff0c;但实现方法不同&#xff1a; H…

【每天一篇深度学习论文】2024多级卷积模块MCM

目录 论文介绍题目&#xff1a;论文地址&#xff1a; 创新点方法模型总体架构双流编码器特征融合模块解码器 核心模块描述多尺度感知融合模块&#xff08;MAFM&#xff09;全局融合模块&#xff08;GFM&#xff09;多级卷积模块&#xff08;MCM&#xff09; 即插即用模块作用特…

Play with docker 使用ssh命令远程登录时Permission denied (publickey)

可以看到这里使用的是 ssh-ed25519 在本机生成对应密钥: ssh-keygen -t ed25519 -P "" -f ~/.ssh/id_ed25519 然后再尝试远程连接就好了。 参考:无法通过SSH连接到码头游乐场中的实例-腾讯云开发者社区-腾讯云

我眼中的“懂重构”(一)

初识重构 2017年的时候&#xff0c;领导让我看公司的一本书《重构——改善代码的既有设计》&#xff0c;这是一本JAVA版本的&#xff0c;前后看了2遍。那时候看书因为不懂看的格外仔细。我只是那时候不懂&#xff0c;然而多年后的今天我仍然发现很多人对重构充满误解。在刚进入…

数字图像处理(15):图像灰度反转和彩色反转

&#xff08;1&#xff09;图像反转&#xff1a;是指对图像的颜色信息进行相反的处理&#xff0c;从而得到一个新的图像。在计算机视觉和图像处理领域&#xff0c;图像反转是一种常见的操作&#xff0c;它可以帮助我们实现不同的图像特效和视觉效果。 &#xff08;2&#xff09…

Ubuntu系统上mysql服务部署

前段时间搞了一个mysql服务端的部署&#xff0c;在Ubuntu系统上&#xff0c;中间也踩了许多坑&#xff0c;特此记录下。 下载 官网&#xff1a;MySQL :: MySQL Community Downloads 这个里面有不同系统的安装包&#xff0c;根据自己的系统选择&#xff0c;我选了 MySQL Com…

linux 服务器 一次性查看 CPU、内存和磁盘使用情况

创建 vi check_usage.sh #!/bin/bashecho " CPU 使用率 " mpstat -P ALL 1 1echo -e "\n 内存使用情况 " free -hecho -e "\n 磁盘使用率 " df -h执行授权 chmod x check_usage.sh执行查看 ./check_usage.sh这样可以快速获取系统资源的概览。…

Unity HDRP Water Surface 水系统 基础教程

Unity HDRP Water Surface 水系统 基础教程 Unity Water SurfaceUnity 项目创建Unity Water Surface&#xff1a;Ocean&#xff08;海洋&#xff09;简介Ocean&#xff1a;Transform、GeneralOcean&#xff1a;Simulation&#xff08;仿真模拟&#xff09;Ocean&#xff1a;Sim…

【Golang】Golang基础语法(三):常量

常量 Golang 语言当中常量的定义和其它语言类似。 const filename_in_package string "abc.txt" // 可以定义为包内常量func consts() {const filename string "abc.txt" // 可以为常量规定类型const a, b 3, 4 // 也可以不规定const…

Cesium-环境搭建

安装步骤 1.安装node.js 2.去Cesium官网下载源码包 other:npm install Cesium 通过这种方式装 ,没有装成功,主要错误提示说缺少gulp文件,具体错误如下 ​ [1/5] Validating package.json... [2/5] Resolving packages... success Already up-to-date. $ gulp prepare &a…

mysql基础学习1

useradd -r -g mysql -s /bin/false mysql (-r)系统用户 不能登录 A temporary password is generated for rootlocalhost: d>#jT7rfoaz) 看是否启动 看进程 端口 直接连接 看日志 varchar (20) char(20)更耗空间 create table student_info(id int,name varchar(20),s…

行业Know-How助力零售企业数字化转型|StartDT Talk

【StartDT Talk】“客户成功三要素”系列直播第三期圆满收官&#xff01; 本期直播聚焦于三要素之一的“好的行业Know-How”&#xff08;行业理解&#xff09;&#xff0c;由奇点云创始人行在和资深产研专家追风共同探讨与零售相关的行业知识&#xff0c;以及我们在零售行业的…

linux——进程间通信system V消息队列

Linux——命名管道及日志-CSDN博客 文章目录 目录 文章目录 前言 一、system V消息队列是什么&#xff1f; 二、相关库接口 1.shmget接口 2、ftok接口 3、shmget、ftok接口封装 4、共享内存操作 ​编辑 5、shmdt接口 三.函数的调用 1、查看共享内存 2、shell 四…

【Redis】not support: redis

1、查看redis进程 2、查看是否安装redis扩展&#xff0c;此处以宝塔为例

网站维护记录

服务器重启&#xff0c;网站打不开&#xff1a;chown -R manager:manager /run/php-fpm/www.sock wordpress升级需设置ftp&#xff1a; // 设置权限0777 //define("FS_METHOD", "direct"); //define("FS_CHMOD_DIR", 0777); //define("…

XiYan-SQL:⼀种多⽣成器集成的Text-to-SQL框架

发布于:2024 年 12 月 03 日 星期二 北京 #NL2SQL #阿里巴巴 #Text-to-SQL 文提出了一种用于自然语言到 SQL 转换的多生成器集成框架 ——XiYan-SQL,旨在应对大型语言模型在 NL2SQL 任务中的挑战。该框架融合提示工程与监督微调(SFT)方法,利用 SFT 的可控性与上下文学习(…

283.移动零(快慢指针)

目录 题目过程解法 题目 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 过程 class Solution { public:void moveZeroes(vector<int…