0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

5種方式初始化String成員怎樣選擇?

CPP開發(fā)者 ? 來源:CPP開發(fā)者 ? 作者:CPP開發(fā)者 ? 2022-11-24 14:51 ? 次閱讀

C++初始化成員的方式有許多,尤其是隨著C++11值類別的重新定義,各種方式之間的差異更是細(xì)微。

本文將以String成員初始化為例,探討以下5種方式之間的優(yōu)劣:
  • call by-const-reference

  • call by-value

  • two-overloads

  • std::string_view

  • forwarding references

輸入不同,它們的開銷也完全不同,我們將以4種不同的輸入分別討論。本篇結(jié)束,下方的表格也將填滿。6c0b8b9c-6bab-11ed-8abf-dac502259ad0.png ?實際要討論的情況遠超1種,因此這張表格在不同的情境下,填入的開銷也不盡相同。多種情況,多個概念,交叉討論,錯綜復(fù)雜,這也是本篇文章難度能入四星的原因。下面正式進入討論。

1call by-const-reference

這種方式最是廣為流傳,C++11之前亦可使用,是早期的推薦方式。

比如:

structS{
std::stringmem;
S(conststd::string&s):mem{s}{}
};
即便現(xiàn)在,使用這種方式也是大有人在。

根據(jù)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。當(dāng)傳入一個字符串字面量時,會先通過隱式構(gòu)造創(chuàng)建一個臨時的string對象,將它綁定到形參之上,再通過拷貝構(gòu)造復(fù)制到成員變量。共2次分配。

第二,lvalue。對于左值,將直接綁定到形參上,再通過拷貝構(gòu)造復(fù)制到成員變量。共1次分配。

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

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

由此可知,使用const-reference string時,至少存在1次分配。對于左值來說,這本無可厚非;但對于右值來說,這將徒增一次沒必要的拷貝;對于字符串字面量,還會由隱式構(gòu)造創(chuàng)建一個臨時對象,增加一次開銷。

因此,這種方式只有針對左值時,效果才不錯,其他情況都會增加一次開銷。

2call by-value

這是從C++11開始也廣為流傳的一種方式,使用的人也不少。4種調(diào)用不變,實現(xiàn)變?yōu)榱耍?/span>
structS{
std::stringmem;
S(std::strings):mem{std::move(s)}{}
};
這種方式采用值傳遞參數(shù),在許多人的印象中,這種開銷很大,實際情況到底如何呢?第一,Implicit ctor。同樣,先通過隱式構(gòu)造創(chuàng)建一個臨時對象,然后將其值偷取到成員變量。共1次分配+1次移動。第二,lvalue??截悓ο螅缓髮⑵渲低等〉匠蓡T變量。共1次分配+1次移動。第三,xvalue。值經(jīng)過兩次偷取到成員變量。共0次分配+2次移動。第四,prvalue。值直接原地構(gòu)造,然后偷取到成員變量。共0次分配+1次移動。可以看到,call by-const-reference的缺點,這種方式全部避免。只在接受左值時,多了一次移動。因此,很多情況下,這種方式往往是一種更好的選擇,它可以避免無效的拷貝。

3Two-overloads

這種方式通過多提供一個移動構(gòu)造來消除call by-const-reference的缺點,由于存在兩個重載函數(shù),所以稱為two-overloads。此時實現(xiàn)變?yōu)榱耍?/span>
structS{
std::stringmem;
S(conststd::string&s):mem{s}{}
S(std::string&&s):mem{std::move(s)}{}
};
相信你已經(jīng)猜到了現(xiàn)在的開銷。第一,Implicit ctor。同樣,先創(chuàng)建一個臨時對象,然后調(diào)用移動構(gòu)造。共1次分配+1次移動。第二,lvalue。調(diào)用拷貝構(gòu)造。共1次分配。第三,xvalue。調(diào)用移動構(gòu)造。共0次分配+2次移動。第四,prvalue。調(diào)用移動構(gòu)造。共0次分配+1次移動。通過多增加一個重載函數(shù),得到了不少好處,因此這也是一種可行的方式,但多寫一個重載函數(shù)總是頗顯瑣碎。

4C++17 string_view

C++17 std::string_view也是一種可行的方案,所謂是又輕又快。采用這種方式,實現(xiàn)變?yōu)椋?/span>
structS{
std::stringmem;
S(std::string_views):mem{s}{}
};
此時的開銷情況如何?第一,Implicit ctor。除了mem創(chuàng)建,沒有多余開銷。共1次分配。第二,lvalue。通過隱式轉(zhuǎn)換創(chuàng)建string_view,然后拷貝到成員變量。共1次分配。第三,xvalue。同上。共1次分配。第四,prvalue。同上。共1次分配。對于右值,這種方式也會產(chǎn)生沒必要的開銷。最重要的是,std::string_view隱藏有許多潛在的危險,就像操作裸指針一樣,需要程序員來確保它的有效性。稍不留神,就有可能產(chǎn)生懸垂引用,指向一個已經(jīng)刪除的string對象。因此,若是對其沒有一定的研究,極有可能使用錯誤的用法。

5Fowarding references

Forwarding references可以自動匹配左值或是右值版本,也是一種不錯的方式。

實現(xiàn)變?yōu)椋?/span>

structS{
std::stringmem;
template<classT>
S(T&&s):mem{std::forward(s)}{}
};
此時的開銷又如何?第一,Implicit ctor。除了mem構(gòu)造,無額外開銷。共1次分配。第二,lvalue。直接綁定到實例化的模板函數(shù)參數(shù)上,然后拷貝一份。共1次分配。第三,xvalue。調(diào)用移動構(gòu)造。共0次分配+2次移動。第四,prvalue。調(diào)用移動構(gòu)造。共0次分配+1次移動。這種方式借助了模板,參數(shù)的實際類型根據(jù)TAD推導(dǎo),所以它的開銷也都很小。很多時候,這種方式就是最佳選擇,它可以避免非必要的移動或是拷貝,也適用于非String成員的初始化。但有些時候,你可能想明確指定參數(shù)類型,此時這種方式就多有不便了。下節(jié)有相應(yīng)例子。

6曲未盡

分析至此,已然可以初步得出一張開銷對比圖。

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

看情況。

沒有哪種方式是完全占優(yōu)的,可以依據(jù)使用次數(shù)最多的操作計算消耗,從而正確決策。

舉個例子:

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中添加數(shù)據(jù)。此時,如何讓浪費最?。?/span>

假設(shè)我們后面使用次數(shù)最多的操作為:

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"});
那么上述實現(xiàn)就是一種較好的方式。對于鍵,如果使用call by-const-reference,將會創(chuàng)建一個沒必要的臨時對象,而使用string_view可以避免此開銷。對于值,實際上也使用隱式構(gòu)造創(chuàng)建了一個臨時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)
));
}
對于鍵來說的確不錯,但對于值來說就存在問題了。因為模板參數(shù)推導(dǎo)為initializer_list,而參數(shù)傳遞需要的是vector,使用這種方式還得手動創(chuàng)建一個臨時的vector。所以,具體問題具體分析,才能選擇最恰當(dāng)?shù)姆绞?,有時甚至可以組合使用。大家也許注意到,開銷對比圖標(biāo)題為"初始化Long String成員開銷圖",那么還有短String嗎?

6.1SSO短字符串優(yōu)化

各家編譯器在實現(xiàn)std::string時,基本都會采取一種SSO(Small String Optimization)策略。

此時,對于短字符串,將不會在堆上額外分配內(nèi)存,而是直接存儲在棧上。比如,有些實現(xiàn)會在size的最低標(biāo)志位上用1代表長字符串,0代表短字符串,根據(jù)這個標(biāo)志位來決定操作形式。

可以通過重載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上得出的結(jié)果為:
stackspace=32,heapspace=0,capacity=15

可以看到,對于短字符串,將不會在堆上分配額外的內(nèi)存,內(nèi)容實際存在在棧上。

早期版本的編譯器可能沒有這種優(yōu)化,但如今的版本基本都有。

也就是說,這時的移動操作實際就相當(dāng)于復(fù)制操作。于是開銷就可以如下圖表示。6c56f794-6bab-11ed-8abf-dac502259ad0.png

于是可以得出結(jié)論:盡管小對象的拷貝操作很快,call by-value還是要慢于其他方式,string_view則是一種較好的方式。

但是,string_view使用起來要格外當(dāng)心,若你不想為此操心,使用call by-const-reference則是一種不錯的方式。

6.2無拷貝,無移動

當(dāng)僅需要接受參數(shù),之后即不拷貝,也無移動的情境下,情況又不一樣。

此時的開銷如下圖。

6c804400-6bab-11ed-8abf-dac502259ad0.png ?前三種方式對于隱式構(gòu)造,都會產(chǎn)生一個臨時對象,故皆含一次分配。后兩種方式?jīng)]有這次額外分配。Views的創(chuàng)建非常之輕,可忽略不計;Forwarding references經(jīng)過TAD推導(dǎo)的參數(shù)為常量串,也無消耗。總的來說,我們能得出,通常情況下,后兩種方式在這種情境下是一種開銷更小的方式。若不考慮隱式構(gòu)造,則除了call by-value,其他方式是一種開銷更小的方式。

6.3優(yōu)化限制:Aliasing situations

Aliasing situations指的是多個變量實際指向的是同一塊內(nèi)存,這些變量之間互為別名。這種情況會導(dǎo)致編譯器束手束腳,不敢優(yōu)化。

以引用的方式傳遞參數(shù)便會產(chǎn)生許多額外的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。

而值傳遞版本,由于參數(shù)進行了拷貝,不存在外部修改,m和n的值肯定相同,于是編譯器可以優(yōu)化為只加載一次s.value。

這是call by-const-reference的另一處缺點,它可能會限制編譯器的優(yōu)化,而這又恰恰成了call by-value的一個優(yōu)點。

7總結(jié)

本篇文章介紹了5種初始化String成員的方式,詳細(xì)分析對比了它們的開銷。

沒有哪種方式是最優(yōu)解,如何選擇需要依具體情況而論。

Two-overloads這種方式一般不會考慮,因為總有其他方式比它的開銷更小,還只需編寫一個函數(shù)。

string_view在很多情況下的開銷可觀,但是需要格外注意潛在的懸垂引用問題。

其他三種方式亦是有利有弊,可根據(jù)文中提及的各種情況進行分析。

總而言之,各方式之間存在著細(xì)微而本質(zhì)的差別,且還有許多特殊情況需要單獨分析,開銷在不同情境下也不盡相同。

一句話,看情況。

審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • C++
    C++
    +關(guān)注

    關(guān)注

    21

    文章

    2100

    瀏覽量

    73453
  • 變量
    +關(guān)注

    關(guān)注

    0

    文章

    613

    瀏覽量

    28306
  • string
    +關(guān)注

    關(guān)注

    0

    文章

    40

    瀏覽量

    4715

原文標(biāo)題:5 種方式初始化 String 成員,怎樣選擇?

文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    SpringBean初始化順序

    monitorMetric; final String string; // 構(gòu)造函數(shù) public Metric (String string) { this .
    的頭像 發(fā)表于 11-06 16:04 ?98次閱讀
    SpringBean<b class='flag-5'>初始化</b>順序

    基于旋轉(zhuǎn)平移解耦框架的視覺慣性初始化方法

    精確和魯棒的初始化對于視覺慣性里程計(VIO)至關(guān)重要,因為不良的初始化會嚴(yán)重降低姿態(tài)精度。
    的頭像 發(fā)表于 11-01 10:16 ?125次閱讀
    基于旋轉(zhuǎn)平移解耦框架的視覺慣性<b class='flag-5'>初始化</b>方法

    TMS320C6000 McBSP初始化

    電子發(fā)燒友網(wǎng)站提供《TMS320C6000 McBSP初始化.pdf》資料免費下載
    發(fā)表于 10-26 10:10 ?0次下載
    TMS320C6000 McBSP<b class='flag-5'>初始化</b>

    esp32調(diào)試MQTT的程序,如何對.host初始化?

    esp_mqtt_client_config_t mqtt_cfg這個結(jié)構(gòu)體的時候,你們例程里面只初始化url,但是我在網(wǎng)上看到很多地方的參考程序都是初始化這些結(jié)構(gòu)體成員的: esp_mqtt_client_config_t
    發(fā)表于 06-11 07:55

    初始化IO口為外部中斷線的時候,最先初始化的會被后初始化的覆蓋掉為什么?

    初始化IO口為外部中斷線的時候,比如GPIOA6與GPIOB6先后初始化為外部中斷,最先初始化的會被后初始化的覆蓋掉,不知道是為什么?
    發(fā)表于 05-14 08:26

    字符型、指針型等變量等該如何初始化

     對于數(shù)值類型的變量往往初始化為0,但對于其他類型的變量,如字符型、指針型等變量等該如何初始化呢?
    的頭像 發(fā)表于 03-18 11:02 ?1208次閱讀

    MCU單片機GPIO初始化該按什么順序配置?為什么初始化時有電平跳變?

    GPIO初始化時有時鐘配置、模式配置、輸出配置、復(fù)用配置,那么在編寫初始化代碼時,到底該按什么順序執(zhí)行呢?如果順序不當(dāng)那初始化過程可能會出現(xiàn)短暫的電平跳變。
    的頭像 發(fā)表于 02-22 11:07 ?1386次閱讀
    MCU單片機GPIO<b class='flag-5'>初始化</b>該按什么順序配置?為什么<b class='flag-5'>初始化</b>時有電平跳變?

    串口初始化一般是初始化哪些內(nèi)容

    串口初始化是指在使用串口進行數(shù)據(jù)通信之前,對串口進行一系列的設(shè)置和配置,以確保串口能夠正常工作。串口初始化的內(nèi)容主要包括以下幾個方面: 串口硬件設(shè)置:首先,需要確定要使用的串口是哪一個,通常計算機
    的頭像 發(fā)表于 01-04 09:39 ?2985次閱讀

    labview運行后如何初始化

    需要創(chuàng)建一個新的項目。在開始菜單中打開LabVIEW軟件,選擇"新建項目",然后選擇一個適合的文件夾來保存項目文件。在項目窗口中,可以添加各種不同的文件、VI(Virtual Instrument虛擬儀器)及其他資源。 確定程序需求: 在進行
    的頭像 發(fā)表于 12-28 17:24 ?2406次閱讀

    自動初始化機制原理詳解

    自動初始化機制是指初始化函數(shù)不需要被顯式調(diào)用,只需要在函數(shù)定義處通過宏定義的方式進行申明,就會在系統(tǒng)啟動過程中被執(zhí)行。這篇文章就來探索一下其中的奧秘, 簡單理解其原理!
    的頭像 發(fā)表于 12-16 09:33 ?947次閱讀
    自動<b class='flag-5'>初始化</b>機制原理詳解

    C語言編程時,各種類型的變量該如何初始化?

    C語言編程時,各種類型的變量該如何初始化? 在C語言中,每個變量都需要在使用之前進行初始化。初始化是為變量分配內(nèi)存空間并賦予初始值的過程。C語言提供了不同的
    的頭像 發(fā)表于 12-07 13:53 ?1094次閱讀

    secondary cpu初始化狀態(tài)設(shè)置

    ,用于填寫secondary cpu的入口地址。 uboot負(fù)責(zé)將這塊內(nèi)存的地址寫入devicetree中,當(dāng)內(nèi)核初始化完成,需要啟動secondary cpu時,就將其內(nèi)核入口地址寫到那塊內(nèi)存中
    的頭像 發(fā)表于 12-05 15:27 ?964次閱讀
    secondary cpu<b class='flag-5'>初始化</b>狀態(tài)設(shè)置

    C語言中的數(shù)組格式與初始化

    多少存儲空間。 數(shù)組格式與初始化 ????格式: ? 元素類型 數(shù)組名[元素個數(shù)]; ? ?????比如: ? int[3]; ? ????數(shù)組元素有順序之分,每個元素都有一個唯一的下標(biāo)(索引),而且都是從0開始。 ????數(shù)組中第i個元素的訪問方式:? ? a[i] ?
    的頭像 發(fā)表于 11-26 16:12 ?735次閱讀
    C語言中的數(shù)組格式與<b class='flag-5'>初始化</b>

    實戰(zhàn)經(jīng)驗 | Keil、IAR、CubeIDE 中變量不被初始化方法

    關(guān)鍵詞:不被初始化,編譯環(huán)境 目錄預(yù)覽 1、前言 2、IAR 實現(xiàn)變量不初始化方法 3、Keil 實現(xiàn)變量不被初始化方法 4、CubeIDE 實現(xiàn)變量不初始化方法 01 前言 有些時候
    的頭像 發(fā)表于 11-24 18:05 ?3751次閱讀

    MSP430F5529硬件IIC驅(qū)動OLED(初始化使用的寄存器)

    MSP430F5529硬件IIC驅(qū)動OLED(初始化使用的寄存器)
    發(fā)表于 11-24 16:36 ?1次下載