歡迎您光臨本站 註冊首頁

C++17結構化綁定的實現

←手機掃碼閱讀     ml5rwbikls @ 2020-06-01 , reply:0

動機

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

 #include#includeint main() { typedef std::mapMap; Map map; std::pairresult = 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 <<"[" <first <<", " <second <<"]" <<std::endl; }

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

 #include#include#includeint main() { std::mapmap; 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; }

但是這種方法仍遠不完美,因為:

  • 變量必須事先單獨聲明,其類型都需顯式表示,無法自動推導;

  • 對於默認構造函數執行零初始化的類型,零初始化的過程是多餘的;

  • 也許根本沒有可用的默認構造函數,如std::ofstream。

為此,C++17引入了結構化綁定(structured binding)。

 #include#includeint main() { std::mapmap; 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; }

結構化綁定這一語言特性在提議的階段曾被稱為分解聲明(decomposition declaration),後來又被改回結構化綁定。這個名字想強調的是,結構化綁定的意義重在綁定而非聲明。

語法

結構化綁定有三種語法:

 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為逗號分隔的標識符,expression為單個表達式。

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

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

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

行為

結構化綁定有三類行為,與上面的三種語法之間沒有對應關係。

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

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

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

應用

結構化綁定擅長處理純數據類型,包括自定義類型與std::tuple等,給實例的每一個字段分配一個變量名:

 #includestruct 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; }

配合其他語法糖,現代C++代碼可以很優雅:

 #include#includeint main() { std::mapmap; 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; }

利用結構化綁定在類元組類型上的行為,我們可以改變數據類型的結構化綁定細節,包括類型轉換、是否拷貝等:

 #include#include#includeclass Transcript { /* ... */ }; class Student { public: const char* name; Transcript score; std::string getName() const { return name; } const Transcript& getScore() const { return score; } templatedecltype(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: std::integral_constant{ }; template<> struct tuple_element{ using type = decltype(std::declval().getName()); }; template<> struct tuple_element{ using type = decltype(std::declval().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是一個數據類型,有兩個字段name和score。name是一個C風格字符串,它大概是從C代碼繼承來的,我希望客戶能用上C++風格的std::string;score屬於Transcript類型,表示學生的成績單,這個結構比較大,我希望能傳遞const引用以避免不必要的拷貝。為此,我寫明瞭三要素:std::tuple_size、std::tuple_element和get。這種機制給了結構化綁定很強的靈活性。

細節

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


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

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

  • 當expression是數組類型A,且ref-operator不存在時,E為cv A,每個元素由expression中的對應元素拷貝(= expression)或直接初始化({ expression }或( expression );

  • 否則,相當於定義e為attr cv-auto ref-operator e initializer;。

也就是說,方括號前面的修飾符都是作用於e的,而不是那些新聲明的變量。至於為什麼第一條會獨立出來,這是因為在標準C++中第二條的形式不能用於數組拷貝。

然後分三種情況討論:

  • 數組情形,E為T的數組類型,則每個結構化綁定都是指向e數組中元素的左值;被引類型(referenced type)為T;――結構化綁定是左值,不是左值引用:int array[2]{ 1, 2 }; auto& [i, j] = array; static_assert(!std::is_reference_v

    );;
  • 類元組情形,如果e是左值引用,則e是左值(lvalue),否則是消亡值(xvalue);記Ti為std::tuple_element

                           ::type,則結構化綁定vi的類型是Ti的引用;當get返回左值引用時是左值引用,否則是右值引用;被引類型為Ti;――decltype對結構化綁定有特殊處理,產生被引類型,在類元組情形下結構化綁定的類型與被引類型是不同的;                
  • 數據成員情形,與數組類似,設數據成員mi被聲明為Ti類型,則結構化綁定的類型是指向cv Ti的左值(同樣不是左值引用);被引類型為cv Ti。

至此,我想“結構化綁定”的意義已經明確了:標識符總是綁定一個對象,該對象是另一個對象的成員(或數組元素),後者或是拷貝或是引用(引用不是對象,意會即可)。與引用類似,結構化綁定都是既有對象的別名(這個對象可能是隱式的);與引用不同,結構化綁定不一定是引用類型。

現在可以解釋ri非const的現象了:編譯器先創建了變量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),結構化綁定支持這樣的數據成員。如果類有union類型成員,它必須是命名的,綁定的標識符的類型為該union類型的左值;如果有未命名的union成員,則這個類不能用於結構化綁定。

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

 #includestruct 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的功能就像位域的引用一樣,既能寫回原值,又不會超出位域的範圍。

還有一些語法細節,比如get的名字查找、std::tuple_size沒有value、explicit拷貝構造函數等,除非是深挖語法的language lawyer,在實際開發中不必糾結(上面這一堆已經可以算language lawyer了吧)。

侷限

以上代碼示例應該已經囊括了所有類型的結構化綁定應用,你能想象到的其他語法都是錯的,包括但不限於:

用std::initializer_list初始化;

因為std::initializer_list的長度是動態的,但結構化綁定的標識符數量是靜態的。

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

這相當於聲明瞭三個變量,但結構化綁定的意圖在於綁定而非聲明。

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

這相當於用std::tie,所以請繼續用std::tie。另外,由[開始可能與attributes混淆,給編譯器和編譯器設計者帶來壓力。

指明結構化綁定的修飾符――auto [& x, const y, const& z] = f();;

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

指明結構化綁定的類型――SomeClass [x, y] = f();或auto [x, std::string y] = f();;

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

顯式忽略一個結構化綁定――auto [x, std::ignore, z] = f();;

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

標識符嵌套――std::tuple<T1, std::pair, T4> f(); auto [ w, [x, y], z ] = f();;

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

以上語法都沒有納入C++20標準,不過可能在將來成為C++語法的擴展。

延伸

C++17的新特性不是孤立的,與結構化綁定相關的有:

類模板參數推導(class template argument deduction,CTAD),由構造函數參數推導類模板參數;

拷貝省略(copy elision),保證NRV(named return value)優化;

constexpr if,簡化泛型代碼,消除部分SFINAE;

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


[ml5rwbikls ] C++17結構化綁定的實現已經有248次圍觀

http://coctec.com/docs/c/language/show-post-236496.html