動機
std::map<K, V>的insert方法回傳std::pair<iterator, bool>,兩個元素分別是指向所插入鍵值對的迭代器與指示是否新插入元素的布林值,而std::map<K, V>::iterator解參考又得到鍵值對std::pair<const K, V>,在一個涉及std::map的演算法中,有可能出現大量的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標準庫添加了std::tie,用若干參考構造出一個std::tuple,對它賦以std::tuple物件可以給其中的參考一一賦值(二元std::tuple可以由std::pair構造或賦值),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;
}
但是這種方法仍遠不完美,因為:
-
變數必須事先單獨宣告,其型別都需顯式表示,無法自動推導;
-
對于默認建構式執行零初始化的型別,零初始化的程序是多余的;
-
也許根本沒有可用的默認建構式,如
std::ofstream,
為此,C++17引入了結構化系結(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;
}
結構化系結這一語言特性在提議的階段曾被稱為分解宣告(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>是一個完整型別,則稱E為類元組(tuple-like)型別,在STL中,std::array、std::pair和std::tuple都是這樣的型別,此時,identifier-list的長度必須與std::tuple_size<E>::value相等,每個識別符號的型別都通過std::tuple_element推匯出(具體見后文),用成員get<I>()或get<I>(e)初始化,顯然,這些標準庫設施是與語言核心系結的,
第三種情況,E是非union型別別,系結非靜態資料成員,所有非靜態資料成員都必須是public訪問屬性,全部在E中,或全部在E的一個基類中(即不能分散在多個類中),identifier-list按照類中非靜態資料成員的宣告順序系結,數量相等,
應用
結構化系結擅長處理純資料型別,包括自定義型別與std::tuple等,給實體的每一個欄位分配一個變數名:
#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;
}
配合其他語法糖,現代C++代碼可以很優雅:
#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;
}
利用結構化系結在類元組型別上的行為,我們可以改變資料型別的結構化系結細節,包括型別轉換、是否拷貝等:
#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是一個資料型別,有兩個欄位name和score,name是一個C風格字串,它大概是從C代碼繼承來的,我希望客戶能用上C++風格的std::string;score屬于Transcript型別,表示學生的成績單,這個結構比較大,我希望能傳遞const參考以避免不必要的拷貝,為此,我寫明了三要素:std::tuple_size、std::tuple_element和get,這種機制給了結構化系結很強的靈活性,
細節
#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;
}
如果結構化系結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<decltype(i)>);; -
類元組情形,如果
e是左值參考,則e是左值(lvalue),否則是消亡值(xvalue);記Ti為std::tuple_element<i, E>::type,則結構化系結vi的型別是Ti的參考;當get回傳左值參考時是左值參考,否則是右值參考;被引型別為Ti;——
decltype對結構化系結有特殊處理,產生被引型別,在類元組情形下結構化系結的型別與被引型別是不同的; -
資料成員情形,與陣列類似,設資料成員
mi被宣告為Ti型別,則結構化系結的型別是指向cv Ti的左值(同樣不是左值參考);被引型別為cv Ti,
至此,我想“結構化系結”的意義已經明確了:識別符號總是系結一個物件,該物件是另一個物件的成員(或陣列元素),后者或是拷貝或是參考(參考不是物件,意會即可),與參考類似,結構化系結都是既有物件的別名(這個物件可能是隱式的);與參考不同,結構化系結不一定是參考型別,
(不理解的話可以參考N4659 11.5節,盡管你很可能會更加看不懂……)
現在可以解釋ri非const的現象了:編譯器先創建了變數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),結構化系結支持這樣的資料成員,如果類有union型別成員,它必須是命名的,系結的識別符號的型別為該union型別的左值;如果有未命名的union成員,則這個類不能用于結構化系結,
C++中不存在位域的指標和參考,但結構化系結可以是指向位域的左值:
#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的功能就像位域的參考一樣,既能寫回原值,又不會超出位域的范圍,
還有一些語法細節,比如get的名字查找、std::tuple_size<E>沒有value、explicit拷貝建構式等,除非是深挖語法的language lawyer,在實際開發中不必糾結(上面這一堆已經可以算language lawyer了吧),
局限
以上代碼示例應該已經囊括了所有型別的結構化系結應用,你能想象到的其他語法都是錯的,包括但不限于:
-
用
std::initializer_list<T>初始化;因為
std::initializer_list<T>的長度是動態的,但結構化系結的識別符號數量是靜態的, -
用串列初始化——
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<T2, T3>, 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)優化;
-
constexprif,簡化泛型代碼,消除部分SFINAE; -
帶初始化的條件分支陳述句:語法糖,使代碼更加優雅,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/27263.html
標籤:C++
下一篇:類歐幾里得演算法
