動(dòng)機(jī)
std::map
的insert
方法返回std::pair
,兩個(gè)元素分別是指向所插入鍵值對(duì)的迭代器與指示是否新插入元素的布爾值,而std::map
解引用又得到鍵值對(duì)std::pair
。在一個(gè)涉及std::map
的算法中,有可能出現(xiàn)大量的first
和second
,讓人不知所措。
#include
#include
C++11標(biāo)準(zhǔn)庫(kù)添加了std::tie
,用若干引用構(gòu)造出一個(gè)std::tuple
,對(duì)它賦以std::tuple
對(duì)象可以給其中的引用一一賦值(二元std::tuple
可以由std::pair
構(gòu)造或賦值)。std::ignore
是一個(gè)占位符,所在位置的賦值被忽略。
#include
#include
但是這種方法仍遠(yuǎn)不完美,因?yàn)椋?/p>
?變量必須事先單獨(dú)聲明,其類型都需顯式表示,無(wú)法自動(dòng)推導(dǎo);?對(duì)于默認(rèn)構(gòu)造函數(shù)執(zhí)行零初始化的類型,零初始化的過程是多余的;?也許根本沒有可用的默認(rèn)構(gòu)造函數(shù),如std::ofstream
。
為此,C++17引入了結(jié)構(gòu)化綁定(structured binding)。
#include
#include
結(jié)構(gòu)化綁定這一語(yǔ)言特性在提議的階段曾被稱為分解聲明(decomposition declaration),后來(lái)又被改回結(jié)構(gòu)化綁定。這個(gè)名字想強(qiáng)調(diào)的是,結(jié)構(gòu)化綁定的意義重在綁定而非聲明。
語(yǔ)法
結(jié)構(gòu)化綁定有三種語(yǔ)法:
attr(optional)cv-autoref-operator(optional)[identifier-list]=expression;
attr(optional)cv-autoref-operator(optional)[identifier-list]{expression};
attr(optional)cv-autoref-operator(optional)[identifier-list](expression);
其中,attr(optional)
為可選的attributes,cv-auto
為可能有const
或volatile
修飾的auto
,ref-operator(optional)
為可選的&
或&&
,identifier-list
為逗號(hào)分隔的標(biāo)識(shí)符,expression
為單個(gè)表達(dá)式。
另外再定義initializer
為= expression
、{ expression }
或( expression )
,換言之上面三種語(yǔ)法有統(tǒng)一的形式attr(optional) cv-auto ref-operator(optional) [ identifier-list ] initializer;
。
整個(gè)語(yǔ)句是一個(gè)結(jié)構(gòu)化綁定聲明,標(biāo)識(shí)符也稱為結(jié)構(gòu)化綁定(structured bindings),不過兩處“binding”的詞性不同。
順帶一提,C++20中volatile
的許多用法都被廢棄了。
行為
結(jié)構(gòu)化綁定有三類行為,與上面的三種語(yǔ)法之間沒有對(duì)應(yīng)關(guān)系。
第一種情況,expression
是數(shù)組,identifier-list
的長(zhǎng)度必須與數(shù)組長(zhǎng)度相等。
第二種情況,對(duì)于expression
的類型E
,std::tuple_size
是一個(gè)完整類型,則稱E
為類元組(tuple-like)類型。在STL中,std::array
、std::pair
和std::tuple
都是這樣的類型。此時(shí),identifier-list
的長(zhǎng)度必須與std::tuple_size
相等,每個(gè)標(biāo)識(shí)符的類型都通過std::tuple_element
推導(dǎo)出(具體見后文),用成員get()
或get(e)
初始化。顯然,這些標(biāo)準(zhǔn)庫(kù)設(shè)施是與語(yǔ)言核心綁定的。
第三種情況,E
是非union
類類型,綁定非靜態(tài)數(shù)據(jù)成員。所有非靜態(tài)數(shù)據(jù)成員都必須是public
訪問屬性,全部在E
中,或全部在E
的一個(gè)基類中(即不能分散在多個(gè)類中)。identifier-list
按照類中非靜態(tài)數(shù)據(jù)成員的聲明順序綁定,數(shù)量相等。
應(yīng)用
結(jié)構(gòu)化綁定擅長(zhǎng)處理純數(shù)據(jù)類型,包括自定義類型與std::tuple
等,給實(shí)例的每一個(gè)字段分配一個(gè)變量名:
#include
structPoint
{
doublex,y;
};
Pointmidpoint(constPoint&p1,constPoint&p2)
{
return{(p1.x+p2.x)/2,(p1.y+p2.y)/2};
}
intmain()
{
Pointp1{1,2};
Pointp2{3,4};
auto[x,y]=midpoint(p1,p2);
std::cout<"("?<
配合其他語(yǔ)法糖,現(xiàn)代C++代碼可以很優(yōu)雅:
#include
#include
利用結(jié)構(gòu)化綁定在類元組類型上的行為,我們可以改變數(shù)據(jù)類型的結(jié)構(gòu)化綁定細(xì)節(jié),包括類型轉(zhuǎn)換、是否拷貝等:
#include
#include
#include
classTranscript{/*...*/};
classStudent
{
public:
constchar*name;
Transcriptscore;
std::stringgetName()const{returnname;}
constTranscript&getScore()const{returnscore;}
template
decltype(auto)get()const
{
ifconstexpr(I==0)
returngetName();
elseifconstexpr(I==1)
returngetScore();
else
static_assert(I2);
????}
};
namespace?std
{
template<>
structtuple_size
:std::integral_constant{};
template<>
structtuple_element<0,?Student>{usingtype=decltype(std::declval().getName());};
template<>
structtuple_element<1,?Student>{usingtype=decltype(std::declval().getScore());};
}
intmain()
{
std::cout<
Student
是一個(gè)數(shù)據(jù)類型,有兩個(gè)字段name
和score
。name
是一個(gè)C風(fēng)格字符串,它大概是從C代碼繼承來(lái)的,我希望客戶能用上C++風(fēng)格的std::string
;score
屬于Transcript
類型,表示學(xué)生的成績(jī)單,這個(gè)結(jié)構(gòu)比較大,我希望能傳遞const
引用以避免不必要的拷貝。為此,我寫明了三要素:std::tuple_size
、std::tuple_element
和get
。這種機(jī)制給了結(jié)構(gòu)化綁定很強(qiáng)的靈活性。
細(xì)節(jié)
#include
#include
#include
intmain()
{
std::pairpair{1,2.0};
intnumber=3;
std::tupletuple(number);
constauto&[i,f]=pair;
//i=4;//error
constauto&[ri]=tuple;
ri=5;
}
如果結(jié)構(gòu)化綁定i
被聲明為const auto&
,對(duì)應(yīng)的類型為int
,那么它應(yīng)該是個(gè)const int&
吧?i = 4;
出錯(cuò)了,看起來(lái)正是如此。但是如何解釋ri = 5;
是合法的呢?
這個(gè)問題需要系統(tǒng)地從頭談起。先引入一個(gè)名字e
,E
為其類型:
?當(dāng)expression
是數(shù)組類型A
,且ref-operator
不存在時(shí),E
為cv A
,每個(gè)元素由expression
中的對(duì)應(yīng)元素拷貝(= expression
)或直接初始化({ expression }
或( expression )
;?否則,相當(dāng)于定義e
為attr cv-auto ref-operator e initializer;
。
也就是說(shuō),方括號(hào)前面的修飾符都是作用于e
的,而不是那些新聲明的變量。至于為什么第一條會(huì)獨(dú)立出來(lái),這是因?yàn)樵跇?biāo)準(zhǔn)C++中第二條的形式不能用于數(shù)組拷貝。
然后分三種情況討論:
?數(shù)組情形,E
為T
的數(shù)組類型,則每個(gè)結(jié)構(gòu)化綁定都是指向e
數(shù)組中元素的左值;被引類型(referenced type)為T
;——結(jié)構(gòu)化綁定是左值,不是左值引用:int array[2]{ 1, 2 }; auto& [i, j] = array; static_assert(!std::is_reference_v
;?類元組情形,如果e
是左值引用,則e
是左值(lvalue),否則是消亡值(xvalue);記Ti
為std::tuple_element::type
,則結(jié)構(gòu)化綁定vi
的類型是Ti
的引用;當(dāng)get
返回左值引用時(shí)是左值引用,否則是右值引用;被引類型為Ti
;——decltype
對(duì)結(jié)構(gòu)化綁定有特殊處理,產(chǎn)生被引類型,在類元組情形下結(jié)構(gòu)化綁定的類型與被引類型是不同的;?數(shù)據(jù)成員情形,與數(shù)組類似,設(shè)數(shù)據(jù)成員mi
被聲明為Ti
類型,則結(jié)構(gòu)化綁定的類型是指向cv Ti
的左值(同樣不是左值引用);被引類型為cv Ti
。
至此,我想“結(jié)構(gòu)化綁定”的意義已經(jīng)明確了:標(biāo)識(shí)符總是綁定一個(gè)對(duì)象,該對(duì)象是另一個(gè)對(duì)象的成員(或數(shù)組元素),后者或是拷貝或是引用(引用不是對(duì)象,意會(huì)即可)。與引用類似,結(jié)構(gòu)化綁定都是既有對(duì)象的別名(這個(gè)對(duì)象可能是隱式的);與引用不同,結(jié)構(gòu)化綁定不一定是引用類型。
(不理解的話可以參考N465911.5節(jié),盡管你很可能會(huì)更加看不懂……)
現(xiàn)在可以解釋ri
非const
的現(xiàn)象了:編譯器先創(chuàng)建了變量const auto& e = tuple;
,E
為const std::tuple
,std::tuple_element<0, E>::type
為int&
,std::get<0>(e)
同樣返回int&
,故ri
為int&
類型。
在面向底層的C++編程中常用union
和位域(bit field),結(jié)構(gòu)化綁定支持這樣的數(shù)據(jù)成員。如果類有union
類型成員,它必須是命名的,綁定的標(biāo)識(shí)符的類型為該union
類型的左值;如果有未命名的union
成員,則這個(gè)類不能用于結(jié)構(gòu)化綁定。
C++中不存在位域的指針和引用,但結(jié)構(gòu)化綁定可以是指向位域的左值:
#include
structBitField
{
intf1:4;
intf2:4;
intf3:4;
};
intmain()
{
BitFieldb{1,2,3};
auto&[f1,f2,f3]=b;
f2=4;
autoprint=[&]{std::cout<
程序輸出:
143
153
f2
的功能就像位域的引用一樣,既能寫回原值,又不會(huì)超出位域的范圍。
還有一些語(yǔ)法細(xì)節(jié),比如get
的名字查找、std::tuple_size
沒有value
、explicit
拷貝構(gòu)造函數(shù)等,除非是深挖語(yǔ)法的language lawyer,在實(shí)際開發(fā)中不必糾結(jié)(上面這一堆已經(jīng)可以算language lawyer了吧)。
局限
以上代碼示例應(yīng)該已經(jīng)囊括了所有類型的結(jié)構(gòu)化綁定應(yīng)用,你能想象到的其他語(yǔ)法都是錯(cuò)的,包括但不限于:
?用std::initializer_list
初始化;因?yàn)?code style="white-space:pre-wrap;line-height:1.75;font-size:13.5px;color:rgb(221,17,68);background:rgba(27,31,35,.05);padding:3px 5px;">std::initializer_listauto [x,y,z] = {1, "xyzzy"s, 3.14159};
;這相當(dāng)于聲明了三個(gè)變量,但結(jié)構(gòu)化綁定的意圖在于綁定而非聲明。?不聲明而直接綁定——[iter, success] = mymap.insert(value);
;這相當(dāng)于用std::tie
,所以請(qǐng)繼續(xù)用std::tie
。另外,由[
開始可能與attributes混淆,給編譯器和編譯器設(shè)計(jì)者帶來(lái)壓力。?指明結(jié)構(gòu)化綁定的修飾符——auto [& x, const y, const& z] = f();
;同樣是脫離了結(jié)構(gòu)化綁定的意圖。如果需要這樣的功能,或者一個(gè)個(gè)定義變量,或者手動(dòng)寫上三要素。?指明結(jié)構(gòu)化綁定的類型——SomeClass [x, y] = f();
或auto [x, std::string y] = f();
;第一種可用auto [x, y] = SomeClass{ f() };
代替;第二種同上一條。?顯式忽略一個(gè)結(jié)構(gòu)化綁定——auto [x, std::ignore, z] = f();
;消除編譯器警告是一個(gè)理由,但是auto [x, y, z] = f(); (void)y;
亦可。這還涉及一些語(yǔ)言問題,請(qǐng)移步P0144R23.8節(jié)。?標(biāo)識(shí)符嵌套——std::tuple
;多寫一行吧。[
同樣可能與attributes混淆。
以上語(yǔ)法都沒有納入C++20標(biāo)準(zhǔn),不過可能在將來(lái)成為C++語(yǔ)法的擴(kuò)展。
延伸
C++17的新特性不是孤立的,與結(jié)構(gòu)化綁定相關(guān)的有:
?類模板參數(shù)推導(dǎo)(class template argument deduction,CTAD),由構(gòu)造函數(shù)參數(shù)推導(dǎo)類模板參數(shù);?拷貝省略(copy elision),保證NRV(named return value)優(yōu)化;?constexpr
if
,簡(jiǎn)化泛型代碼,消除部分SFINAE;?帶初始化的條件分支語(yǔ)句:語(yǔ)法糖,使代碼更加優(yōu)雅。
-
代碼
+關(guān)注
關(guān)注
30文章
4810瀏覽量
68827 -
編譯器
+關(guān)注
關(guān)注
1文章
1640瀏覽量
49200 -
結(jié)構(gòu)化
+關(guān)注
關(guān)注
0文章
27瀏覽量
10326
原文標(biāo)題:聊聊結(jié)構(gòu)化綁定
文章出處:【微信號(hào):CPP開發(fā)者,微信公眾號(hào):CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論