0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

5种方式初始化String成员怎样选择?

CPP开发者 来源:CPP开发者 作者:CPP开发者 2022-11-24 14:51 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

C++初始化成员的方式有许多,尤其是随着C++11值类别的重新定义,各种方式之间的差异更是细微。

本文将以String成员初始化为例,探讨以下5种方式之间的优劣:
  • call by-const-reference

  • call by-value

  • two-overloads

  • std::string_view

  • forwarding references

输入不同,它们的开销也完全不同,我们将以4种不同的输入分别讨论。本篇结束,下方的表格也将填满。6c0b8b9c-6bab-11ed-8abf-dac502259ad0.png  实际要讨论的情况远超1种,因此这张表格在不同的情境下,填入的开销也不尽相同。多种情况,多个概念,交叉讨论,错综复杂,这也是本篇文章难度能入四星的原因。下面正式进入讨论。

1call by-const-reference

这种方式最是广为流传,C++11之前亦可使用,是早期的推荐方式。

比如:

structS{
std::stringmem;
S(conststd::string&s):mem{s}{}
};
即便现在,使用这种方式也是大有人在。

根据4种不同的输入分析其开销,代码如下:

std::stringstr{"dummy"};
Ss1("dummy");//1.Implicitctor
Ss2(str);//2.lvalue
Ss3(std::move(str));//3.xvalue
Ss4(std::string{"dummy"});//4.prvalue
第一,Implicit ctor。当传入一个字符串字面量时,会先通过隐式构造创建一个临时的string对象,将它绑定到形参之上,再通过拷贝构造复制到成员变量。共2次分配。

第二,lvalue。对于左值,将直接绑定到形参上,再通过拷贝构造复制到成员变量。共1次分配。

第三,xvalue。对于消亡值,操作同上。共1次分配。

第四,prvalue。对于纯右值,操作同上。共1次分配。

由此可知,使用const-reference string时,至少存在1次分配。对于左值来说,这本无可厚非;但对于右值来说,这将徒增一次没必要的拷贝;对于字符串字面量,还会由隐式构造创建一个临时对象,增加一次开销。

因此,这种方式只有针对左值时,效果才不错,其他情况都会增加一次开销。

2call by-value

这是从C++11开始也广为流传的一种方式,使用的人也不少。4种调用不变,实现变为了:
structS{
std::stringmem;
S(std::strings):mem{std::move(s)}{}
};
这种方式采用值传递参数,在许多人的印象中,这种开销很大,实际情况到底如何呢?第一,Implicit ctor。同样,先通过隐式构造创建一个临时对象,然后将其值偷取到成员变量。共1次分配+1次移动。第二,lvalue。拷贝对象,然后将其值偷取到成员变量。共1次分配+1次移动。第三,xvalue。值经过两次偷取到成员变量。共0次分配+2次移动。第四,prvalue。值直接原地构造,然后偷取到成员变量。共0次分配+1次移动。可以看到,call by-const-reference的缺点,这种方式全部避免。只在接受左值时,多了一次移动。因此,很多情况下,这种方式往往是一种更好的选择,它可以避免无效的拷贝。

3Two-overloads

这种方式通过多提供一个移动构造来消除call by-const-reference的缺点,由于存在两个重载函数,所以称为two-overloads。此时实现变为了:
structS{
std::stringmem;
S(conststd::string&s):mem{s}{}
S(std::string&&s):mem{std::move(s)}{}
};
相信你已经猜到了现在的开销。第一,Implicit ctor。同样,先创建一个临时对象,然后调用移动构造。共1次分配+1次移动。第二,lvalue。调用拷贝构造。共1次分配。第三,xvalue。调用移动构造。共0次分配+2次移动。第四,prvalue。调用移动构造。共0次分配+1次移动。通过多增加一个重载函数,得到了不少好处,因此这也是一种可行的方式,但多写一个重载函数总是颇显琐碎。

4C++17 string_view

C++17 std::string_view也是一种可行的方案,所谓是又轻又快。采用这种方式,实现变为:
structS{
std::stringmem;
S(std::string_views):mem{s}{}
};
此时的开销情况如何?第一,Implicit ctor。除了mem创建,没有多余开销。共1次分配。第二,lvalue。通过隐式转换创建string_view,然后拷贝到成员变量。共1次分配。第三,xvalue。同上。共1次分配。第四,prvalue。同上。共1次分配。对于右值,这种方式也会产生没必要的开销。最重要的是,std::string_view隐藏有许多潜在的危险,就像操作裸指针一样,需要程序员来确保它的有效性。稍不留神,就有可能产生悬垂引用,指向一个已经删除的string对象。因此,若是对其没有一定的研究,极有可能使用错误的用法。

5Fowarding references

Forwarding references可以自动匹配左值或是右值版本,也是一种不错的方式。

实现变为:

structS{
std::stringmem;
template<classT>
S(T&&s):mem{std::forward(s)}{}
};
此时的开销又如何?第一,Implicit ctor。除了mem构造,无额外开销。共1次分配。第二,lvalue。直接绑定到实例化的模板函数参数上,然后拷贝一份。共1次分配。第三,xvalue。调用移动构造。共0次分配+2次移动。第四,prvalue。调用移动构造。共0次分配+1次移动。这种方式借助了模板,参数的实际类型根据TAD推导,所以它的开销也都很小。很多时候,这种方式就是最佳选择,它可以避免非必要的移动或是拷贝,也适用于非String成员的初始化。但有些时候,你可能想明确指定参数类型,此时这种方式就多有不便了。下节有相应例子。

6曲未尽

分析至此,已然可以初步得出一张开销对比图。

6c2b0968-6bab-11ed-8abf-dac502259ad0.png因此,若是要问哪种方式初始化String成员比较好,如何回答?

看情况。

没有哪种方式是完全占优的,可以依据使用次数最多的操作计算消耗,从而正确决策。

举个例子:

structS{
usingvalue_type=std::vector<std::string>;
usingassoc_type=std::map<std::string,value_type>;

voidpush_data(std::string_viewkey,value_typedata){
datasets.emplace(std::make_pair(key,std::move(data)));
}

assoc_typedatasets;
};
功能很简单,就是往一个map中添加数据。此时,如何让浪费最小?

假设我们后面使用次数最多的操作为:

Ss;
s.push_data("key1",{"Dear","Friend"});
s.push_data("key2",{"Apple"});
s.push_data("key3",{"Jack","Tom","Jerry"});
s.push_data("key4",{"20","22","11","20"});
那么上述实现就是一种较好的方式。对于键,如果使用call by-const-reference,将会创建一个没必要的临时对象,而使用string_view可以避免此开销。对于值,实际上也使用隐式构造创建了一个临时vector对象,此时call by-value也是一种开销较小的方式。你可能觉得Forwarding reference也是一种不错的方式。
voidpush_data(auto&&key,auto&&data){
datasets.emplace(std::make_pair(
std::forward<decltype(key)>(key),
std::forward<decltype(data)>(data)
));
}
对于键来说的确不错,但对于值来说就存在问题了。因为模板参数推导为initializer_list,而参数传递需要的是vector,使用这种方式还得手动创建一个临时的vector。所以,具体问题具体分析,才能选择最恰当的方式,有时甚至可以组合使用。大家也许注意到,开销对比图标题为"初始化Long String成员开销图",那么还有短String吗?

6.1SSO短字符串优化

各家编译器在实现std::string时,基本都会采取一种SSO(Small String Optimization)策略。

此时,对于短字符串,将不会在堆上额外分配内存,而是直接存储在栈上。比如,有些实现会在size的最低标志位上用1代表长字符串,0代表短字符串,根据这个标志位来决定操作形式。

可以通过重载operator new和operator delete来捕获堆分配情况,一个例子如下:
std::size_tallocated=0;

void*operatornew(size_tsz){
void*p=std::malloc(sz);
allocated+=sz;
returnp;
}

voidoperatordelete(void*p)noexcept{
returnstd::free(p);
}

intmain(){
allocated=0;
std::strings("hi");
std::printf("stackspace=%zu,heapspace=%zu,capacity=%zu
",
sizeof(s),allocated,s.capacity());
}
例子来源:https://stackoverflow.com/a/28003328
在clang 14.0.0上得出的结果为:
stackspace=32,heapspace=0,capacity=15

可以看到,对于短字符串,将不会在堆上分配额外的内存,内容实际存在在栈上。

早期版本的编译器可能没有这种优化,但如今的版本基本都有。

也就是说,这时的移动操作实际就相当于复制操作。于是开销就可以如下图表示。6c56f794-6bab-11ed-8abf-dac502259ad0.png

于是可以得出结论:尽管小对象的拷贝操作很快,call by-value还是要慢于其他方式,string_view则是一种较好的方式。

但是,string_view使用起来要格外当心,若你不想为此操心,使用call by-const-reference则是一种不错的方式。

6.2无拷贝,无移动

当仅需要接受参数,之后即不拷贝,也无移动的情境下,情况又不一样。

此时的开销如下图。

6c804400-6bab-11ed-8abf-dac502259ad0.png  前三种方式对于隐式构造,都会产生一个临时对象,故皆含一次分配。后两种方式没有这次额外分配。Views的创建非常之轻,可忽略不计;Forwarding references经过TAD推导的参数为常量串,也无消耗。总的来说,我们能得出,通常情况下,后两种方式在这种情境下是一种开销更小的方式。若不考虑隐式构造,则除了call by-value,其他方式是一种开销更小的方式。

6.3优化限制:Aliasing situations

Aliasing situations指的是多个变量实际指向的是同一块内存,这些变量之间互为别名。这种情况会导致编译器束手束脚,不敢优化。

以引用的方式传递参数便会产生许多额外的Aliasing situations。

举个例子:

intfoo_by_ref(constS&s){
intm=s.value;
bar();
intn=s.value;
returnm+n;
}

intfoo_by_value(Ss){
intm=s.value;
bar();
intn=s.value;
returnm+n;
}

例子来源:https://reductor.dev/cpp/2022/06/27/pass-by-value-vs-pass-by-reference.html

引用传递版本,编译器无法判定bar()中是否修改了s.value,比如s引用的是一个全局变量,bar()中就可以修改它。因此,m和n的值可能并不相同,编译器必须加载两次s.value。

而值传递版本,由于参数进行了拷贝,不存在外部修改,m和n的值肯定相同,于是编译器可以优化为只加载一次s.value。

这是call by-const-reference的另一处缺点,它可能会限制编译器的优化,而这又恰恰成了call by-value的一个优点。

7总结

本篇文章介绍了5种初始化String成员的方式,详细分析对比了它们的开销。

没有哪种方式是最优解,如何选择需要依具体情况而论。

Two-overloads这种方式一般不会考虑,因为总有其他方式比它的开销更小,还只需编写一个函数。

string_view在很多情况下的开销可观,但是需要格外注意潜在的悬垂引用问题。

其他三种方式亦是有利有弊,可根据文中提及的各种情况进行分析。

总而言之,各方式之间存在着细微而本质的差别,且还有许多特殊情况需要单独分析,开销在不同情境下也不尽相同。

一句话,看情况。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C++
    C++
    +关注

    关注

    22

    文章

    2122

    浏览量

    76709
  • 变量
    +关注

    关注

    0

    文章

    615

    浏览量

    29369
  • string
    +关注

    关注

    0

    文章

    40

    浏览量

    5018

原文标题:5 种方式初始化 String 成员,怎样选择?

文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    瀚海微SD NAND TF卡硬件识别与初始化类问题探讨

    在瀚海微SD NAND/TF卡的实际应用中,硬件识别与初始化是保障设备正常运行的首要环节,该环节出现故障会直接导致存储卡无法投入使用,尤其在工业控制、车载设备等关键场景中,可能引发设备停机、数据丢失
    的头像 发表于 11-18 09:58 124次阅读
    瀚海微SD NAND  TF卡硬件识别与<b class='flag-5'>初始化</b>类问题探讨

    串口通信基石:Air8000下Modbus RTU串口初始化详解!

    串口初始化是Modbus RTU通信稳定可靠的关键起点。本篇深入剖析Air8000开发板串口参数配置、初始化代码实现及调试要点,结合master_rtu示例,分享实际操作中的经验与避坑指南,让您
    的头像 发表于 10-28 16:33 1440次阅读
    串口通信基石:Air8000下Modbus RTU串口<b class='flag-5'>初始化</b>详解!

    GraniStudio:IO初始化以及IO资源配置例程

    1.文件运行 导入工程 双击运行桌面GraniStudio.exe。 通过引导界面导入IO初始化以及IO资源配置例程,点击导入按钮。 打开IO初始化以及IO资源配置例程所在路径,选中IO初始化以及
    的头像 发表于 08-22 17:34 746次阅读
    GraniStudio:IO<b class='flag-5'>初始化</b>以及IO资源配置例程

    GraniStudio:初始化例程

    1.文件运行 导入工程 双击运行桌面GraniStudio.exe。 通过引导界面导入初始化例程,点击导入按钮。 打开初始化例程所在路径,选中初始化.gsp文件,点击打开,完成导入。 2.功能
    的头像 发表于 08-22 16:45 704次阅读
    GraniStudio:<b class='flag-5'>初始化</b>例程

    MCU外设初始化:为什么参数配置必须优先于使能

    在微控制器领域,初始化参数配置阶段至关重要。此时,虽无电源驱动,但微控制器在使能信号到来前,借初始化参数配置这一精细步骤,开启关键准备进程。初始化参数配置如同物理坐标锚定、逻辑指令部署、内在秩序预设
    的头像 发表于 08-13 10:38 567次阅读

    定义IO初始化结构体

    由上述IOPORT相关功能的枚举类型我们可以知道,在对IOPORT模块进行初始化时需要根据情况配置它们。因此我们定义一个IOPORT初始化的结构体类型IOPORT_Init_t,它的成员包括了由上述所有枚举类型所声明的变量,因此
    的头像 发表于 07-16 16:26 1107次阅读

    IM系列设备过载故障:界面初始化终止的诊断流程与修复指南

    当 IM 系列设备出现过载故障导致界面初始化终止时,通常表现为设备启动后操作界面无显示、指示灯异常闪烁或初始化进度条停滞不前,同时可能伴随设备运行异响或异常发热。初步判断时,可观察设备是否有错
    的头像 发表于 06-28 09:47 938次阅读

    IM 系列设备过载保护机制下界面初始化中断的底层逻辑与解决方案

    一、过载保护机制与界面初始化的关联基础 IM 系列设备的过载保护机制是保障设备安全运行的核心功能,其通过传感器实时采集设备运行参数,如电流、电压、温度、系统资源占用率等。一旦这些参数超出预设阈值
    的头像 发表于 06-27 09:58 366次阅读

    使用PyQt5自动初始化OpenVINO™环境出现报错怎么解决?

    编写了一个程序以使用 PyQt5 自动初始化OpenVINO™环境:从 PyQt5 导入 QtWidgets 导入操作系统 #import时间 导入系统 如果__name__
    发表于 03-07 06:35

    STM32CubeMX用于STM32配置和初始化C代码生成

    电子发烧友网站提供《STM32CubeMX用于STM32配置和初始化C代码生成.pdf》资料免费下载
    发表于 02-26 17:32 678次下载

    EE-359:ADSP-CM40x启动时间优化和器件初始化

    电子发烧友网站提供《EE-359:ADSP-CM40x启动时间优化和器件初始化.pdf》资料免费下载
    发表于 01-13 16:33 0次下载
    EE-359:ADSP-CM40x启动时间优化和器件<b class='flag-5'>初始化</b>

    EE-88:使用21xx编译器在C中初始化变量

    电子发烧友网站提供《EE-88:使用21xx编译器在C中初始化变量.pdf》资料免费下载
    发表于 01-13 15:54 0次下载
    EE-88:使用21xx编译器在C中<b class='flag-5'>初始化</b>变量

    OMAP5912多媒体处理器初始化参考指南

    电子发烧友网站提供《OMAP5912多媒体处理器初始化参考指南.pdf》资料免费下载
    发表于 12-17 16:20 0次下载
    OMAP5912多媒体处理器<b class='flag-5'>初始化</b>参考指南

    STM32F407 MCU使用SD NAND 不断电初始化失效解决方案

    STM32F407微控制器单元(MCU)与SD NAND的结合提供了强大的存储解决方案。然而,不断电初始化失效问题可能会导致系统稳定性和数据完整性受损。我们将STM32F407与SD NAND集成时可能遇到的初始化问题,并提供专业的解决方案。
    的头像 发表于 12-11 10:51 1478次阅读
    STM32F407 MCU使用SD NAND 不断电<b class='flag-5'>初始化</b>失效解决方案

    segger编译器初始化问题

    的图; 2.第二张图是该变量在文件中的所有操作,第一行是初始化,该行代码在初始化部分最后一个,执行完就是主循环了; 3.第2,3行也是对变量的赋值,在主循环中; 4.第4行已注释; 5.第6行是对变量
    发表于 12-09 18:06