最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

結(jié)構(gòu)化綁定詳解

2023-08-30 22:26 作者:JuvwxYZ  | 我要投稿

結(jié)構(gòu)化綁定是C++17新增的語法,適當(dāng)使用能極大地提升編程體驗。結(jié)構(gòu)化綁定將引入的標(biāo)識符綁定到對象的元素或成員上。很多人將結(jié)構(gòu)化綁定視為引用的語法糖,誠然它們有許多相似之處,但二者在語義上還是有很多不同的地方。

對象、引用、結(jié)構(gòu)化綁定是C++中三個相互關(guān)聯(lián)但是彼此并列的概念,它們都是程序中的實體。

1. 綁定到數(shù)組的元素

首先,結(jié)構(gòu)化綁定能夠綁定到數(shù)組的元素上:

在這段代碼中,定義了?a?b?c?三個結(jié)構(gòu)化綁定。通過?printf?可以觀察到它們的值分別是?1?2?3,對應(yīng)數(shù)組arr的三個元素。

實際上在這個過程中編譯器幫我們干了下面的事:首先,引入一個匿名變量,在這里我們叫它_unnamed_,它的各個元素從?arr?復(fù)制初始化。然后,將結(jié)構(gòu)化綁定引入的三個名字分別綁定到這個匿名數(shù)組的三個元素上。

需要注意的是,這里的引用僅僅表示一種綁定關(guān)系,即?a?綁定到_unnamed_[0],并不代表?a?是個引用。例如,我們直接定義一個引用?int &ra = arr[0]?,那么?decltype(ra)?會得到?int&,而結(jié)構(gòu)化綁定的聲明類型是不帶引用的:decltype(a)?得到的是?int。

那么如何證明上述匿名變量的存在,并且真的發(fā)生了復(fù)制呢?一個簡單的辦法是修改?a?的值,arr[0]?并不會隨之變化,說明?a?和?arr[0]指代的兩個不同的對象。當(dāng)然還有更直觀的辦法,那就是自定義類的復(fù)制構(gòu)造函數(shù)。

運(yùn)行上述代碼,我們很能夠觀察到程序輸出下列內(nèi)容:

分割線之前是數(shù)組?arr?的三個元素從?int?直接初始化的輸出,分割線之后是結(jié)構(gòu)化綁定過程中匿名數(shù)組各個元素的復(fù)制構(gòu)造的輸出。并且復(fù)制的對象的地址也能一一對應(yīng)。

但是,這只能證明發(fā)生了復(fù)制,還不足以證明這個匿名變量的存在。編譯器完全可以省略匿名變量,直接從數(shù)組的三個元素復(fù)制初始化三個變量,并且還可以避免前文提到的看起來像引用,卻又不是引用的綁定。

2. 綁定到數(shù)據(jù)成員

讓我們帶著這個問題來看看下面這段代碼:

程序的輸出如下:

可以看到,這里只調(diào)用了一次B的復(fù)制構(gòu)造,說明這個匿名變量確實存在。

和數(shù)組類似,結(jié)構(gòu)化綁定可以綁定到類的非靜態(tài)數(shù)據(jù)成員。當(dāng)然并不是每一種類都可以被結(jié)構(gòu)化綁定,它必須有如下性質(zhì):

  1. 它所有的非靜態(tài)數(shù)據(jù)成員在當(dāng)前語境中可訪問。

  2. 它所有的非靜態(tài)數(shù)據(jù)成員都是它自己,或者同一個基類的直接成員。

結(jié)構(gòu)化綁定并不要求成員必須有?public?訪問權(quán)限,只要在當(dāng)前語境中可以訪問所有成員即可。

第二點似乎不太直觀,我們通過兩個例子來說明一下:

我們從?A?派生出?B?和?C?兩個類。其中?B?沒有增加任何數(shù)據(jù)成員,它可能只添加了一些成員函數(shù)擴(kuò)展了?A?的功能,這在開發(fā)中也是很常見的手法。那么對于?B?來說,它所有的非靜態(tài)數(shù)據(jù)成員都是從基類?A?繼承而來的,因此?B?符合結(jié)構(gòu)化綁定的要求。而?C?則增加了一個數(shù)據(jù)成員,因此不滿足第二條性質(zhì)。

3. 結(jié)構(gòu)化綁定中的限定符

左值引用限定符

剛才我們見到的結(jié)構(gòu)化綁定都有一個復(fù)制的過

程,會產(chǎn)生一個匿名對象。有時候復(fù)制的開銷會比較大,我們當(dāng)然想避免不必要的復(fù)制。于是我們可以為結(jié)構(gòu)化綁定添加一個引用限定符,以引用的方式綁定到相應(yīng)的對象上。

還記得剛剛說過結(jié)構(gòu)化綁定過程中的匿名變量嗎?它再一次派上大用場了。如果結(jié)構(gòu)化綁定聲明中包含引用限定符,那么這個引入的匿名變量就是一個引用!

引用?_unnamed_?綁定到?arr,而?a?又綁定到?_unnamed_[0],也就是說?a?直接綁定到了?arr[0]?上。b?和?c?同理。再一次強(qiáng)調(diào),即使添加了引用限定符,結(jié)構(gòu)化綁定也不是引用,decltype(a)?仍然是?int?而不是?int&。這里的引用只是為了表達(dá)綁定關(guān)系。

定義引用不會產(chǎn)生可觀察的副作用,我們也就無法直接證明這個匿名變量確實是引用。當(dāng)然我們還是可以從側(cè)面來應(yīng)證它,比如說左值引用不能綁定到右值。

cv限定符

很自然地,你會想到用右值引用來綁定到右值表達(dá)式,但別忘了const限定的左值引用,它們也可以綁定到右值。

加上?const?限定之后,我們就不能修改這些結(jié)構(gòu)化綁定的值了。在需要的時候加上?const?能讓我們的程序更加安全。

既然是cv限定符,自然還有?volatile。我們稍微提一下,這個限定符實際上很少用到,甚至在C++20中棄用了大部分語境中的?volatile?限定,包括結(jié)構(gòu)化綁定。你仍然可以寫,但編譯器可能會發(fā)出警告。volatile?是另一個很大的話題,并且涉及到很多實現(xiàn)上的細(xì)節(jié),這里就不展開講了。

右值引用限定符

實際上在結(jié)構(gòu)化綁定中說“右值引用”限定符并不準(zhǔn)確,畢竟前面還有一個?auto?占位符呢。auto&&?是不是右值引用可說不準(zhǔn),讓我們來復(fù)習(xí)一下。

在上面的示例代碼中,auto&& lref = i?會進(jìn)行類型推導(dǎo),由于初始化器i是個左值,推導(dǎo)出?auto -> int&?再經(jīng)過引用折疊?int& && -> int&?最終得到?lref?是個左值引用。

結(jié)構(gòu)化綁定引入的匿名變量也是如此,如果引用限定符是?&&?那么匿名變量的類型就會根據(jù)這一規(guī)則自動推導(dǎo),這也是?auto&&?被稱為萬能引用的原因。

存儲類說明符(C++20起)

從C++20開始,你可以為結(jié)構(gòu)化綁定加上?static?或者?thread_local?這兩個存儲類說明符,它們同樣是作用在引入的匿名變量上。

使用這兩個說明符的時候要注意,如果再加上引用限定符,綁定到某個局部變量上,很容易產(chǎn)生懸垂引用。這與普通的靜態(tài)變量規(guī)則是相同的。

4. 初始化器

上面的代碼中我們一直都是使用等于號形式的初始化器。實際上結(jié)構(gòu)化綁定還允許花括號和圓括號初始化。大多數(shù)情況下,它們區(qū)別不大。唯一的區(qū)別在于初始化匿名變量的時候,等于號的形式使用復(fù)制初始化,而花括號或者圓括號的形式使用直接初始化。復(fù)制初始化不考慮?explicit?構(gòu)造函數(shù)。

5. 綁定到元組式類型的元素

結(jié)構(gòu)化綁定還能綁定到例如?std::tuple?或者?std::pair,甚至?std::array?這些類型上。但仔細(xì)想想,就會發(fā)現(xiàn)事情并沒有那么簡單。pair?還能用下面這個形式強(qiáng)行解釋一下,結(jié)構(gòu)化綁定是綁定到它的兩個數(shù)據(jù)成員上。

但?tuple?呢?它有公開可訪問的數(shù)據(jù)成員嗎?標(biāo)準(zhǔn)庫似乎沒有提供給我們。訪問?tuple?的元素必須通過?std::get?函數(shù)。如果你了解一點模板元編程,那你應(yīng)該知道?tuple?通常是用模板遞歸繼承的方式實現(xiàn)的,它的數(shù)據(jù)成員分布在一層一層的基類里。這明顯是不符合結(jié)構(gòu)化綁定綁定到數(shù)據(jù)成員的要求的。

再說說?std::array,雖然它名字就叫?array,長的像數(shù)組,用起來也像數(shù)組,但它畢竟不是數(shù)組,std::is_array<std::array<int,3>>::value肯定是?false。

那么結(jié)構(gòu)化綁定是如何實現(xiàn)的呢?其實,C++為我們提供了一套精fù妙zá
的機(jī)制,可以自定義結(jié)構(gòu)化綁定規(guī)則,我們通常叫它元組式綁定。

如果你只是使用標(biāo)準(zhǔn)庫提供的這些元組式類型,那么不必?fù)?dān)心:就把std::pairstd::tuple當(dāng)成所有成員都能公開訪問的結(jié)構(gòu)體,把std::array當(dāng)成普通的數(shù)組。標(biāo)準(zhǔn)庫已經(jīng)給你實現(xiàn)好了相關(guān)的細(xì)節(jié),不了解這套機(jī)制的工作原理也不影響你使用。

使用例:

程序輸出:

如果你對其中的細(xì)節(jié)感興趣,或者想要給你寫的類實現(xiàn)自定義結(jié)構(gòu)化綁定,就讓我們開始吧。

首先,編譯器會檢查結(jié)構(gòu)化綁定的初始化表達(dá)式的類型,我們暫時稱它為?T。如果?T?是數(shù)組類型,那就按照前文所述的規(guī)則綁定到數(shù)組元素。否則,編譯器就會檢查?std::tuple_size<T>::value?是否是一個合法整數(shù)類型的常量表達(dá)式。如果它是,那就進(jìn)行元組式綁定。否則,就按照前文所述的規(guī)則綁定到?T?的數(shù)據(jù)成員。

std::tuple_size?是標(biāo)準(zhǔn)庫中聲明的一個類模板,此外,標(biāo)準(zhǔn)庫還提供了針對?std::pair,std::tuplestd::array?的特化。

上述代碼只是實現(xiàn)?std::tuple_size?特化的方式之一,僅作為示例。如果你要給自定義類型實現(xiàn)結(jié)構(gòu)化綁定,第一步就是寫一個相應(yīng)的?std::tuple_size?特化。它必須包含一個靜態(tài)的整數(shù)常量成員,名字為?value。它的值必須是正整數(shù),表示可以結(jié)構(gòu)化綁定的元素的數(shù)量,如果它的值和[ 標(biāo)識符列表 ]的數(shù)量不相等,則編譯器會報錯。慣例上將它的類型設(shè)定為?size_t,但任意整數(shù)類型都是可以的。

然后,編譯器同樣會引入一個匿名變量來保存初始化表達(dá)式的值。我們以std::tuple為例看看下面的代碼:

auto [i,c,d] = std::make_tuple(1, '2', 3.0);auto _unnamed_ = std::make_tuple(1, '2', 3.0);

為了將結(jié)構(gòu)化綁定中的標(biāo)識符綁定到某個對象,編譯器還會為每一個標(biāo)識符引入一個新的變量。它的類型是?std::tuple_element<0, T>::type的引用。如果它對應(yīng)的初始化表達(dá)式的值類別是左值,那么它是左值引用,否則,它是右值引用。它對應(yīng)的初始化表達(dá)式的形式見后文詳述。

這也就意味著,為了實現(xiàn)自定義結(jié)構(gòu)化綁定,我們還需要自己實現(xiàn)相應(yīng)的?std::tuple_element?特化。它有兩個模板參數(shù),第一個參數(shù)是一個整數(shù),表示結(jié)構(gòu)化綁定的標(biāo)識符的序號,從0開始遞增;第二個參數(shù)是你的自定類型。以?std::pair?為例,看看?std::tuple_element?的自定義特化要怎么寫:

針對?std::tuple?的特化實現(xiàn)起來較為復(fù)雜,需要用到模板遞歸繼承,這里就不作展示了。感興趣的讀者可以自行查找資料,或者翻看STL的源代碼。

總結(jié)來說,std::tuple_element<I, T>::type表示了類型?T?的第?I?個可綁定元素的類型。

有了類型,為每個結(jié)構(gòu)化綁定引入了額外的引用變量之后,接下來就要對這些變量進(jìn)行初始化了,畢竟引用必須在定義的時候就初始化。首先,編譯器會去找類型?T?是否有名為?get?的成員函數(shù)模板,并且?get?的第一個模板參數(shù)是非類型模板參數(shù)。如果找到這樣的成員,那么就調(diào)用?_unnamed_.get<I>()?來初始化第I個變量。如果沒有這樣的成員,就調(diào)用?get<I>(_unnamed_)來初始化,并且查找?get?的過程只進(jìn)行實參依賴查找(ADL, Argument Dependent Lookup),不考慮其他形式。

另外,在調(diào)用?get?的時候,如果匿名變量?_unnamed_?的類型是左值引用,則調(diào)用過程中它保持為左值;否則將它轉(zhuǎn)換到亡值再調(diào)用。也就是說如果get同時存在接受左值引用和右值引用的重載時,前者調(diào)用左值引用的版本,而后者調(diào)用右值引用的版本,這實際上類似于完美轉(zhuǎn)發(fā)。

最后,將結(jié)構(gòu)化綁定引入的標(biāo)識符綁定到額外引入的這些變量所指代的對象上。

最后,我們通過一個例子來看看完整的自定義結(jié)構(gòu)化綁定過程。考慮如下場景:標(biāo)準(zhǔn)庫在常用數(shù)學(xué)函數(shù)庫中提供了?div_t div(int, int)?函數(shù)。它計算兩個整數(shù)相除得到的商和余數(shù),并通過一個結(jié)構(gòu)體返回。但是標(biāo)準(zhǔn)并未規(guī)定結(jié)構(gòu)體?div_t?兩個成員的順序,因此直接綁定到數(shù)據(jù)成員可能會導(dǎo)致順序不對,于是我們可以為它定義一套元組式的綁定方式,讓第一個變量始終綁定到商,而第二個變量始終綁定到余數(shù)。

首先,我們需要為?std::tuple_size<T>?寫一個特化。此處的?std::tuple_size<div_t>::value?即結(jié)構(gòu)化綁定能綁定的成員的數(shù)量,因此我們將它設(shè)置為2。

然后,我們需要為?std::tuple_element?這個模板類寫一些特化,用于確定各個元素的類型。div_t?只有兩個成員,我們直接寫兩個全特化即可。

最后,我們需要寫一個?get?函數(shù),用來綁定匿名變量的各個元素。此處的constexpr if也是C++17的新特性,它的條件表達(dá)式必須是一個編譯期常量,因此它會在編譯期就能根據(jù)條件選擇相應(yīng)的分支,直接將另一個分支刪除,有點類似于預(yù)處理指令?#ifdef-#else-#endif?的效果。

對于我們這個簡單的例子constexpr if并不是必須的,因為這里的if兩個分支返回的類型是相同的。如果if兩個分支返回不同的類型,就可以通過constexpr if消除不需要的分支,保證編譯能夠通過。

完整的示例代碼如下:

完整語法

存儲類說明符?:(C++20起)

static
thread_local

cv限定符?:

const
volatile(C++20起棄用)
const volatile(C++20起棄用)

引用限定符?:

&
&&

初始化器?:

=初始化表達(dá)式
{初始化表達(dá)式}
(初始化表達(dá)式)

結(jié)構(gòu)化綁定聲明?:

存儲類說明符????cv限定符????auto?引用限定符????[標(biāo)識符列表]?初始化器?;

最后,歡迎來到QQ頻道交流討論。
頻道名稱:std::forward編程社區(qū)
搜索頻道號直達(dá):wxj6l1350o

結(jié)構(gòu)化綁定詳解的評論 (共 條)

分享到微博請遵守國家法律
德昌县| 锦屏县| 梧州市| 柘荣县| 枣庄市| 阿坝县| 塘沽区| 辽阳市| 临夏市| 喜德县| 景洪市| 山东| 广南县| 林甸县| 绵阳市| 泰来县| 凤凰县| 墨竹工卡县| 璧山县| 噶尔县| 红河县| 泾川县| 夹江县| 阿拉善左旗| 甘德县| 乌拉特中旗| 平昌县| 达孜县| 富顺县| 香港| 左权县| 新源县| 武威市| 平湖市| 隆回县| 顺平县| 外汇| 金川县| 中江县| 密山市| 贞丰县|