溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C++17如何實(shí)現(xiàn)結(jié)構(gòu)化綁定

發(fā)布時間:2020-07-22 10:42:13 來源:億速云 閱讀:162 作者:小豬 欄目:編程語言

這篇文章主要講解了C++17如何實(shí)現(xiàn)結(jié)構(gòu)化綁定,內(nèi)容清晰明了,對此有興趣的小伙伴可以學(xué)習(xí)一下,相信大家閱讀完之后會有幫助。

動機(jī)

std::map<K, V>的insert方法返回std::pair<iterator, bool>,兩個元素分別是指向所插入鍵值對的迭代器與指示是否新插入元素的布爾值,而std::map<K, V>::iterator解引用又得到鍵值對std::pair<const K, V>。在一個涉及std::map的算法中,有可能出現(xiàn)大量的first和second,讓人不知所措。

#include <iostream>
#include <map>

int main()
{
  typedef std::map<int, int> Map;
  Map map;
  std::pair<Map::iterator, bool> result = map.insert(Map::value_type(1, 2));
  if (result.second)
    std::cout << "inserted successfully" << std::endl;
  for (Map::iterator iter = map.begin(); iter != map.end(); ++iter)
    std::cout << "[" << iter->first << ", " << iter->second << "]" << std::endl;
}

C++11標(biāo)準(zhǔn)庫添加了std::tie,用若干引用構(gòu)造出一個std::tuple,對它賦以std::tuple對象可以給其中的引用一一賦值(二元std::tuple可以由std::pair構(gòu)造或賦值)。std::ignore是一個占位符,所在位置的賦值被忽略。

#include <iostream>
#include <map>
#include <utility>

int main()
{
  std::map<int, int> map;
  bool inserted;
  std::tie(std::ignore, inserted) = map.insert({1, 2});
  if (inserted)
    std::cout << "inserted successfully" << std::endl;
  for (auto&& kv : map)
    std::cout << "[" << kv.first << ", " << kv.second << "]" << std::endl;
}

但是這種方法仍遠(yuǎn)不完美,因?yàn)椋?/p>

  • 變量必須事先單獨(dú)聲明,其類型都需顯式表示,無法自動推導(dǎo);
  • 對于默認(rèn)構(gòu)造函數(shù)執(zhí)行零初始化的類型,零初始化的過程是多余的;
  • 也許根本沒有可用的默認(rèn)構(gòu)造函數(shù),如std::ofstream。

為此,C++17引入了結(jié)構(gòu)化綁定(structured binding)。

#include <iostream>
#include <map>

int main()
{
  std::map<int, int> map;
  auto&& [iter, inserted] = map.insert({1, 2});
  if (inserted)
    std::cout << "inserted successfully" << std::endl;
  for (auto&& [key, value] : map)
    std::cout << "[" << key << ", " << value << "]" << std::endl;
}

結(jié)構(gòu)化綁定這一語言特性在提議的階段曾被稱為分解聲明(decomposition declaration),后來又被改回結(jié)構(gòu)化綁定。這個名字想強(qiáng)調(diào)的是,結(jié)構(gòu)化綁定的意義重在綁定而非聲明。

語法

結(jié)構(gòu)化綁定有三種語法:

attr(optional) cv-auto ref-operator(optional) [ identifier-list ] = expression;
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] { expression };
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] ( expression );

其中,attr(optional)為可選的attributes,cv-auto為可能有const或volatile修飾的auto,ref-operator(optional)為可選的&或&&,identifier-list為逗號分隔的標(biāo)識符,expression為單個表達(dá)式。

另外再定義initializer為= expression、{ expression }或( expression ),換言之上面三種語法有統(tǒng)一的形式attr(optional) cv-auto ref-operator(optional) [ identifier-list ] initializer;。

整個語句是一個結(jié)構(gòu)化綁定聲明,標(biāo)識符也稱為結(jié)構(gòu)化綁定(structured bindings),不過兩處“binding”的詞性不同。

順帶一提,C++20中volatile的許多用法都被廢棄了。

行為

結(jié)構(gòu)化綁定有三類行為,與上面的三種語法之間沒有對應(yīng)關(guān)系。

第一種情況,expression是數(shù)組,identifier-list的長度必須與數(shù)組長度相等。

第二種情況,對于expression的類型E,std::tuple_size<E>是一個完整類型,則稱E為類元組(tuple-like)類型。在STL中,std::array、std::pair和std::tuple都是這樣的類型。此時,identifier-list的長度必須與std::tuple_size<E>::value相等,每個標(biāo)識符的類型都通過std::tuple_element推導(dǎo)出(具體見后文),用成員get<I>()或get<I>(e)初始化。顯然,這些標(biāo)準(zhǔn)庫設(shè)施是與語言核心綁定的。

第三種情況,E是非union類類型,綁定非靜態(tài)數(shù)據(jù)成員。所有非靜態(tài)數(shù)據(jù)成員都必須是public訪問屬性,全部在E中,或全部在E的一個基類中(即不能分散在多個類中)。identifier-list按照類中非靜態(tài)數(shù)據(jù)成員的聲明順序綁定,數(shù)量相等。

應(yīng)用

結(jié)構(gòu)化綁定擅長處理純數(shù)據(jù)類型,包括自定義類型與std::tuple等,給實(shí)例的每一個字段分配一個變量名:

#include <iostream>

struct Point
{
  double x, y;
};

Point midpoint(const Point& p1, const Point& p2)
{
  return { (p1.x + p2.x) / 2, (p1.y + p2.y) / 2 };
}

int main()
{
  Point p1{ 1, 2 };
  Point p2{ 3, 4 };
  auto [x, y] = midpoint(p1, p2);
  std::cout << "(" << x << ", " << y << ")" << std::endl;
}

配合其他語法糖,現(xiàn)代C++代碼可以很優(yōu)雅:

#include <iostream>
#include <map>

int main()
{
  std::map<int, int> map;
  if (auto&& [iter, inserted] = map.insert({ 1, 2 }); inserted)
    std::cout << "inserted successfully" << std::endl;
  for (auto&& [key, value] : map)
    std::cout << "[" << key << ", " << value << "]" << std::endl;
}

利用結(jié)構(gòu)化綁定在類元組類型上的行為,我們可以改變數(shù)據(jù)類型的結(jié)構(gòu)化綁定細(xì)節(jié),包括類型轉(zhuǎn)換、是否拷貝等:

#include <iostream>
#include <string>
#include <utility>

class Transcript { /* ... */ };

class Student
{
public:
  const char* name;
  Transcript score;
  std::string getName() const { return name; }
  const Transcript& getScore() const { return score; }
  template<std::size_t I>
  decltype(auto) get() const
  {
    if constexpr (I == 0)
      return getName();
    else if constexpr (I == 1)
      return getScore();
    else
      static_assert(I < 2);
  }
};

namespace std
{
template<>
struct tuple_size<Student>
  : std::integral_constant<std::size_t, 2> { };

template<>
struct tuple_element<0, Student> { using type = decltype(std::declval<Student>().getName()); };

template<>
struct tuple_element<1, Student> { using type = decltype(std::declval<Student>().getScore()); };
}

int main()
{
  std::cout << std::boolalpha;
  Student s{ "Jerry", {} };
  const auto& [name, score] = s;
  std::cout << name << std::endl;
  std::cout << (&score == &s.score) << std::endl;
}

Student是一個數(shù)據(jù)類型,有兩個字段name和score。name是一個C風(fēng)格字符串,它大概是從C代碼繼承來的,我希望客戶能用上C++風(fēng)格的std::string;score屬于Transcript類型,表示學(xué)生的成績單,這個結(jié)構(gòu)比較大,我希望能傳遞const引用以避免不必要的拷貝。為此,我寫明了三要素:std::tuple_size、std::tuple_element和get。這種機(jī)制給了結(jié)構(gòu)化綁定很強(qiáng)的靈活性。

細(xì)節(jié)

#include <iostream>
#include <utility>
#include <tuple>

int main()
{
  std::pair pair{ 1, 2.0 };
  int number = 3;
  std::tuple<int&> tuple(number);
  const auto& [i, f] = pair;
  //i = 4; // error
  const auto& [ri] = tuple;
  ri = 5;
}

如果結(jié)構(gòu)化綁定i被聲明為const auto&,對應(yīng)的類型為int,那么它應(yīng)該是個const int&吧?i = 4;出錯了,看起來正是如此。但是如何解釋ri = 5;是合法的呢?

這個問題需要系統(tǒng)地從頭談起。先引入一個名字e,E為其類型:

  • 當(dāng)expression是數(shù)組類型A,且ref-operator不存在時,E為cv A,每個元素由expression中的對應(yīng)元素拷貝(= expression)或直接初始化({ expression }或( expression );
  • 否則,相當(dāng)于定義e為attr cv-auto ref-operator e initializer;。

也就是說,方括號前面的修飾符都是作用于e的,而不是那些新聲明的變量。至于為什么第一條會獨(dú)立出來,這是因?yàn)樵跇?biāo)準(zhǔn)C++中第二條的形式不能用于數(shù)組拷貝。

然后分三種情況討論:

  • 數(shù)組情形,E為T的數(shù)組類型,則每個結(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<decltype(i)>);;
  • 類元組情形,如果e是左值引用,則e是左值(lvalue),否則是消亡值(xvalue);記Ti為std::tuple_element<i, E>::type,則結(jié)構(gòu)化綁定vi的類型是Ti的引用;當(dāng)get返回左值引用時是左值引用,否則是右值引用;被引類型為Ti;——decltype對結(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ù)組元素),后者或是拷貝或是引用(引用不是對象,意會即可)。與引用類似,結(jié)構(gòu)化綁定都是既有對象的別名(這個對象可能是隱式的);與引用不同,結(jié)構(gòu)化綁定不一定是引用類型。

現(xiàn)在可以解釋ri非const的現(xiàn)象了:編譯器先創(chuàng)建了變量const auto& e = tuple;,E為const std::tuple<int&>&,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)識符的類型為該union類型的左值;如果有未命名的union成員,則這個類不能用于結(jié)構(gòu)化綁定。

C++中不存在位域的指針和引用,但結(jié)構(gòu)化綁定可以是指向位域的左值:

#include <iostream>

struct BitField
{
  int f1 : 4;
  int f2 : 4;
  int f3 : 4;
};

int main()
{
  BitField b{ 1, 2, 3 };
  auto& [f1, f2, f3] = b;
  f2 = 4;
  auto print = [&] { std::cout << b.f1 << " " << b.f2 << " " << b.f3 << std::endl; };
  print();
  f2 = 21;
  print();
}

程序輸出:

1 4 3
1 5 3

f2的功能就像位域的引用一樣,既能寫回原值,又不會超出位域的范圍。

還有一些語法細(xì)節(jié),比如get的名字查找、std::tuple_size<E>沒有value、explicit拷貝構(gòu)造函數(shù)等,除非是深挖語法的language lawyer,在實(shí)際開發(fā)中不必糾結(jié)(上面這一堆已經(jīng)可以算language lawyer了吧)。

局限

以上代碼示例應(yīng)該已經(jīng)囊括了所有類型的結(jié)構(gòu)化綁定應(yīng)用,你能想象到的其他語法都是錯的,包括但不限于:

用std::initializer_list<T>初始化;

因?yàn)閟td::initializer_list<T>的長度是動態(tài)的,但結(jié)構(gòu)化綁定的標(biāo)識符數(shù)量是靜態(tài)的。

用列表初始化——auto [x,y,z] = {1, "xyzzy"s, 3.14159};;

這相當(dāng)于聲明了三個變量,但結(jié)構(gòu)化綁定的意圖在于綁定而非聲明。

不聲明而直接綁定——[iter, success] = mymap.insert(value);;

這相當(dāng)于用std::tie,所以請繼續(xù)用std::tie。另外,由[開始可能與attributes混淆,給編譯器和編譯器設(shè)計(jì)者帶來壓力。

指明結(jié)構(gòu)化綁定的修飾符——auto [& x, const y, const& z] = f();;

同樣是脫離了結(jié)構(gòu)化綁定的意圖。如果需要這樣的功能,或者一個個定義變量,或者手動寫上三要素。

指明結(jié)構(gòu)化綁定的類型——SomeClass [x, y] = f();或auto [x, std::string y] = f();;

第一種可用auto [x, y] = SomeClass{ f() };代替;第二種同上一條。

顯式忽略一個結(jié)構(gòu)化綁定——auto [x, std::ignore, z] = f();;

消除編譯器警告是一個理由,但是auto [x, y, z] = f(); (void)y;亦可。這還涉及一些語言問題,請移步P0144R2 3.8節(jié)。

標(biāo)識符嵌套——std::tuple<T1, std::pair<T2, T3>, T4> f(); auto [ w, [x, y], z ] = f();;

多寫一行吧。[同樣可能與attributes混淆。

以上語法都沒有納入C++20標(biāo)準(zhǔn),不過可能在將來成為C++語法的擴(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,簡化泛型代碼,消除部分SFINAE;

帶初始化的條件分支語句:語法糖,使代碼更加優(yōu)雅。

看完上述內(nèi)容,是不是對C++17如何實(shí)現(xiàn)結(jié)構(gòu)化綁定有進(jìn)一步的了解,如果還想學(xué)習(xí)更多內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI