今天我想要介紹 C++ 17 新增的 Structured Binding(結構化綁定)。以 std::pair
為例,Structured Binding 能讓我們能直接將 std::pair
的內容綁定到我們指定的識別字:
auto [a, b] = std::make_pair("str", 0);
在這個例子𥚃,a
是型別為 const char *
的 "str"
而 b
是型別為 int
的 0
。換句話說,上面的程式碼相當於:
const char *a = "str";
int b = 0;
這讓我們能簡化「接收 std::pair
」的程式碼。舉例來說,std::set::insert(const value_type &)
函式的回傳型別是 std::pair<iterator, bool>
。回傳值分別代表插入元素的位置與是否已經有相同的元素。我們能使用以下程式碼接收回傳值:
void example(std::set<std::string> &s, const std::string &x) {
auto [iter, inserted] = s.insert(x);
// ...
}
在 C++ 11 要做類似的事可以使用 std::tie
:
void example(std::set<std::string> &s, const std::string &x) {
std::set<std::string>::iterator iter;
bool inserted;
std::tie(iter, inserted) = s.insert(x);
// ...
}
或者自行以 .first
與 .second
存取回傳值:
void example(std::set<std::string> &s, const std::string &x) {
std::pair<std::set<std::string>::iterator, bool> p = s.insert(x);
std::set<std::string>::iterator iter = p.first;
bool inserted = p.second;
// ...
}
三者相比之下,可見 Structured Binding 能簡化不少程式碼。
std::tuple
C++ 11 的 std::tuple
與 std::pair
相似,兩者都能儲存不同型別的元素。兩者的差異是前者依據樣版參數能儲存 0 至 N 個元素,而後者只能儲存 2 個元素。二者比較如下表:
std::tuple | std::pair | 說明 |
---|---|---|
std::make_tuple() |
std::make_pair() |
以函式參數建構一個 tuple 或 pair |
std::get<0>(tuple) |
pair.first |
存取第一個元素 |
std::get<1>(tuple) |
pair.second |
存取第二個元素 |
std::get<i>(tuple) |
存取第 i 個元素 |
Structured Binding 當然也能綁定 std::tuple
的元素:
#include <tuple>
int main() {
auto [x, y, z] = std::make_tuple("str", 0.5, 1);
}
Structured Binding 的識別字數量必需與 std::tuple
的元素數量相等,否則會產生編譯錯誤:
#include <tuple>
int main() {
auto [x, y] = std::make_tuple("str", 0.5, 1);
// test.cpp:4:8: error: only 2 names provided for structured binding
// test.cpp:4:8: note: while ‘std::tuple<const char*, int, char>’ decomposes into 3 elements
}
初始化述句
Structured Binding 述句是一種初始化述句,所以能被用於 for 述句、if 述句與 switch 述句:
for (init-statement condition; step) statement
if (init-statement condition) statement
switch (init-statement condition) statement
它讓我們繞過初始化述句只能定義一種型別的限制:
#include <iostream>
#include <string>
#include <tuple>
std::tuple<int, std::string> test() {
return std::make_tuple(1, "some failure message");
}
int main() {
for (auto [i, tag] = std::make_tuple(0, "tag: ");
i < 10; ++i) {
std::cout << tag << i << std::endl;
}
if (auto [err, cause] = test(); err != 0) {
std::cerr << "error: " << cause << std::endl;
}
}
結構化綁定
這個小節我要深入介紹 Structured Binding(結構化綁定)的語法與語意。
語法
Structured Binding 的語法如下:
const-qualifier-opt auto ref-qualifier-opt [identifiers-list] = assignment-expression;
每個部分分述如下:
- const-qualifier-opt 是
const
關鍵字(可選擇) - ref-qualifier-opt 是
&
或&&
(可選擇) - identifiers-list 是以逗號分隔的關鍵字列表
- assignment-expression 是會被綁定的值。assignment-expression 可以是任何「優先度大於或等於賦值運算子(=)」的運算子構成的表達式。除去逗號運算子與
throw
表達式,assignment-expression 幾乎涵蓋所有 C++ 的表達式。
語意
編譯器會先將 Structured Binding 改寫為:
const-qualifier-opt auto ref-qualifier-opt
__internal_unique_id
= assignment-expression;
其中 __internal_unique_id
是編譯器內部給予的獨特識別字(不會與其他變數撞名)。如果 ref-qualifier-opt 被省略,則會複製一份 assignment-expression 的計算結果。
接著,如果 assignment-expression 的型別遵循 Structured Binding Protocol(結構化綁定協議):
- 特化
std::tuple_size<Type>
類別樣版以提供元素個數。 - 特化
std::tuple_element<Index, Type>
類別樣版以提供各個元素的型別。 - 定義
get
函式樣版作為存取元素的介面。
編譯器會優先採用這些資訊。不過這比較複雜,所以保留到下一節再介紹。
如果 assignment-expression 的型別是一般的 struct 或 class 且所有非靜態成員變數(Non-static Data Member)都是公開的(Public),則 identifiers-list 的識別字個數必須與成員變數的個數一致,各個識別字會依序對應到各個成員變數。例如:
struct Point {
int x_;
int y_;
};
int main() {
auto [a, b] = Point{1, 2};
return a + b;
}
相當於:
int main() {
auto __internal_unique_id = Point{1, 2};
return __internal_unique_id.x_ +
__internal_unique_id.y_;
}
注意:Structured Binding 定義的識別字不是「參考(Reference)」。編譯器會把這些識別字對應為成員變數存取表達式。如果我們用 std::is_reference<decltype(a)>::value
測試 a
的型別,我們會得到 false
。這是為了讓 Structured Binding 的結果和「成員變數存取表達式」的結果一致:
#include <iostream>
#include <type_traits>
struct Point {
int x_;
int y_;
};
int main() {
Point p{1, 2};
auto [a, b] = p;
std::cout << std::is_reference<decltype(a)>::value << " "
<< std::is_reference<decltype(p.x_)>::value
<< std::endl;
return 0;
}
如果 assignment-expression 的型別是陣列,則 identifiers-list 的識別字個數必須與陣列長度一致,各個識別字會依序對應到各個陣列元素:
int main() {
int my_array[] = {1, 2, 3};
auto [a, b, c] = my_array;
return a + c;
}
以上程式碼相當於:
int main() {
int my_array[] = {1, 2, 3};
int __internal_unique_id[3];
std::copy(my_array, my_array + 3, __internal_unique_id);
return __internal_unique_id[0] +
__internal_unique_id[2];
}
備註:因為原生陣列沒有複製建構式,當 assignment-expression 是陣列型別時,C++ 語言另外規定應以元素對元素的方式複製陣列元素。
結構化綁定與參考
接著我們看看 const-qualifier-opt 與 ref-qualifier-opt 對 Structured Binding 的影響。前面我們提到 Structured Binding 述句:
const-qualifier-opt auto ref-qualifier-opt [identifiers-list] = assignment-expression;
會先被改寫為:
const-qualifier-opt auto ref-qualifier-opt
__internal_unique_id
= assignment-expression;
這和 C++ 11 引入的 auto 型別推導是一樣的,const-qualifier-opt 與 ref-qualifier-opt 的組合與效果分述如下表:
const-qualifier-opt | ref-qualifier-opt | 說明 | |
---|---|---|---|
常用 | __internal_unique_id 會是 assignment-expression 計算結果的複本。對 identifiers-list 的改動只會反應於 __internal_unique_id 上。 |
||
& | __internal_unique_id 會是指向 assignment-expression 計算結果的 Lvalue Reference(左值參考)。對 identifiers-list 的改動會反應於 assignment-expression 的計算結果上。因為 __internal_unique_id 是 Lvalue Reference,所以它不能綁定暫時物件。 |
||
最常用 | && | __internal_unique_id 會是 Forward Reference,亦稱為 Universal Reference。依據 assignment-expression 計算結果的型別,這可以是 Lvalue Reference(左值參考)、Rvalue Reference(右值參考)、Constant Reference to Lvalue(左值常數參考)、Constant Reference to Rvalue(右值常數參考)。這是用途最廣泛使用限制最少的組合。 |
|
const | __internal_unique_id 會是 assignment-expression 計算結果的常數複本。 |
||
const | & | __internal_unique_id 會是指向 assignment-expression 計算結果的 Constant Reference to Lvalue(左值常數參考)。 |
|
const | && | __internal_unique_id 會是指向 assignment-expression 計算結果的 Constant Reference to Rvalue。這不是 Forward Reference 也不能綁定 Lvalue(右值)。 |
覺得上表太複雜了嗎?如果無法一次記住所有組合,只要記住最常用的分別是 auto && 與 auto。我之後會另外為文介紹 C++ 11 引入的 Rvalue Reference 與型別推導機制。
自訂 Structured Binding
如果想要自訂 Structured Binding 的行為,或者想要使用 Structured Binding 綁定帶有私有成員變數的物件,你必須:
- 特化
std::tuple_size<Type>
類別樣版以提供元素個數。 - 特化
std::tuple_element<Index, Type>
類別樣版以提供各個元素的型別。 - 定義四個
get
函式樣版作為存取元素的介面。
本節將以 example::MyClass
為例,示範如何自行定義結構化綁定:
namespace example {
class MyClass {
private:
const char *x_;
int y_;
public:
MyClass(const char *x, int y) : x_(x), y_(y) {}
};
} // namespace example
第一步:我們必需特化 std::tuple_size
類別樣版:
#include <cstddef>
#include <tuple>
#include <type_traits>
namespace example {
class MyClass; // Forward Declaration
} // namespace example
namespace std {
template <>
class tuple_size<example::MyClass>
: public integral_constant<size_t, 2> {}
} // namespace std
因為 std::tuple_size
宣告於 <tuple>
標頭檔,所以我們必須先引入 <tuple>
。為了讓我們的特化滿足 C++ 標準的要求,所以我們直接繼承 std::integral_constant
,並分別以 std::size_t
與 2
為樣版參數。這個定義能讓編譯器知道 Structured Binding 會需要 2 個識別字。
第二步:我們必須分別為 x_
與 y_
特化 std::tuple_element
類別樣版:
namespace std {
template <>
class tuple_element<0, example::MyClass> {
public:
using type = const char *;
};
template <>
class tuple_element<1, example::MyClass> {
public:
using type = int;
};
} // namespace std
特化 std::tuple_element
的時候必需定義一個名稱為 type
的型別別名。這個型別通常就是成員變數的型別。
第三步:我們必須宣告四種 get
函式樣版,分別處理 example::MyClass
的左值參考、左值常數參考、右值參考與右值常數參考。另外,因為宣告 get
函式樣版時使用的 std::tuple_element_t
別名樣版(Alias Template)依賴前面定義的 std::tuple_element
類別樣版特化,所以 example::MyClass
在此之前都只有前置宣告。我們現在才要定義 example::MyClass
:
namespace example {
class MyClass {
private:
const char *x_;
int y_;
public:
MyClass(const char *x, int y) : x_(x), y_(y) {}
template <size_t Index>
std::tuple_element_t<Index, MyClass> &get() &;
template <size_t Index>
const std::tuple_element_t<Index, MyClass> &get() const &;
template <size_t Index>
std::tuple_element_t<Index, MyClass> &&get() &&;
template <size_t Index>
const std::tuple_element_t<Index, MyClass> &&get() const &&;
};
template <>
std::tuple_element_t<0, MyClass> &MyClass::get<0>() & {
return x_;
}
template <>
const std::tuple_element_t<0, MyClass> &MyClass::get<0>() const & {
return x_;
}
template <>
std::tuple_element_t<0, MyClass> &&MyClass::get<0>() && {
return std::move(x_);
}
template <>
const std::tuple_element_t<0, MyClass> &&MyClass::get<0>() const && {
return std::move(x_);
}
template <>
std::tuple_element_t<1, MyClass> &MyClass::get<1>() & {
return y_;
}
template <>
const std::tuple_element_t<1, MyClass> &MyClass::get<1>() const & {
return y_;
}
template <>
std::tuple_element_t<1, MyClass> &&MyClass::get<1>() && {
return std::move(y_);
}
template <>
const std::tuple_element_t<1, MyClass> &&MyClass::get<1>() const && {
return std::move(y_);
}
} // namespace example
以上程式碼有兩個值得注意的地方:
get<Index>
成員函式之後分別有&
、const &
、&&
與const &&
。這是分別代表this
指標指向的物件是左值、左值常數、右值與右值常數。get<0>
與get<1>
的函式樣版特化必須寫在類別宣告之外。這是 C++ 語言的規定。
第四步:使用 Structured Binding 綁定 example::MyClass
物件:
#include <iostream>
int main() {
auto [x, y] = example::MyClass("example", 42);
std::cout << x << std::endl;
std::cout << y << std::endl;
return 0;
}
上面的程式碼相當於以下示意程式碼:
int main() {
auto __internal_unique_id = example::MyClass("example", 42);
/* auto && */x = __internal_unique_id.template get<0>();
/* auto && */y = __internal_unique_id.template get<1>();
std::cout << x << std::endl;
std::cout << y << std::endl;
return 0;
}
編譯器會將 Structured Binding 述句翻譯為數個 get<Index>
函式呼叫,然後將 identifiers-list 裡面的識別字對應到 get<Index>
函式的回傳值。雖然這個對應關係有點像是參考(Reference),但是這些識別字本身不是參考(Reference)。識別字與回傳值的對應關係記錄於編譯器內部的資料結構。它無法以 C++ 語言描述。上面的程式碼只是示意偽代碼。
第三步也可以將 get<Index>
從成員函式改寫為 Free Function(自由函式)[1]。不過因為 get<Index>
要存取的變數是私有成員變數,所以我們要先將這些 Free Function 宣告為 Friend Function(友誼函式):
namespace example {
template <size_t Index>
std::tuple_element_t<Index, MyClass> &get(MyClass &);
template <size_t Index>
const std::tuple_element_t<Index, MyClass> &get(const MyClass &);
template <size_t Index>
std::tuple_element_t<Index, MyClass> &&get(MyClass &&) ;
template <size_t Index>
const std::tuple_element_t<Index, MyClass> &&get(const MyClass &&);
class MyClass {
private:
const char *x_;
int y_;
public:
MyClass(const char *x, int y) : x_(x), y_(y) {}
template <size_t Index>
friend std::tuple_element_t<Index, MyClass> &
get(MyClass &);
template <size_t Index>
friend const std::tuple_element_t<Index, MyClass> &
get(const MyClass &);
template <size_t Index>
friend std::tuple_element_t<Index, MyClass> &&
get(MyClass &&) ;
template <size_t Index>
friend const std::tuple_element_t<Index, MyClass> &&
get(const MyClass &&);
};
template <>
std::tuple_element_t<0, MyClass> &get<0>(MyClass &obj) {
return obj.x_;
}
template <>
const std::tuple_element_t<0, MyClass> &get<0>(const MyClass &obj) {
return obj.x_;
}
template <>
std::tuple_element_t<0, MyClass> &&get<0>(MyClass &&obj) {
return std::move(obj.x_);
}
template <>
const std::tuple_element_t<0, MyClass> &&get<0>(const MyClass &&obj) {
return std::move(obj.x_);
}
template <>
std::tuple_element_t<1, MyClass> &get<1>(MyClass &obj) {
return obj.y_;
}
template <>
const std::tuple_element_t<1, MyClass> &get<1>(const MyClass &obj) {
return obj.y_;
}
template <>
std::tuple_element_t<1, MyClass> &&get<1>(MyClass &&obj) {
return std::move(obj.y_);
}
template <>
const std::tuple_element_t<1, MyClass> &&get<1>(const MyClass &&obj) {
return std::move(obj.y_);
}
} // namespace example
要注意的是這些 get
函式樣版必需定義於 example::MyClass
類別所在的命名空間。這樣編譯器在翻譯 Structured Binding 述句時,才能使用「類似」Argument Dependent Lookup 的方法找到對應的 get<Index>
函式。以下程式碼:
int main() {
auto [x, y] = example::MyClass("example", 42);
std::cout << x << std::endl;
std::cout << y << std::endl;
return 0;
}
會被翻譯為:
int main() {
auto __internal_unique_id = example::MyClass("example", 42);
/* auto && */x = example::get<0>(__internal_unique_id);
/* auto && */y = example::get<1>(__internal_unique_id);
std::cout << x << std::endl;
std::cout << y << std::endl;
return 0;
}
附帶一提,前面我們說到編譯器在翻譯 Structured Binding 述句時,會使用「類似」Argument Dependent Lookup 的方式,從參數型別的命名空間尋找 get
函式樣版。然而這和真正的 Argument Dependent Lookup 有一個細微的差異:Argument Dependent Lookup 並不會將函式樣版(Function Template)納入候選清單。C++ 語言規格在描述 Structured Binding 的時候,有特別指明兩者的差異。
如果我們自己要呼叫 get
函式樣版,我們不能依賴 Argument Dependent Lookup。我們必需要以範疇解析運算子(Scoped Resolution Operator)指稱命名空間或者事先以 using
述句引入 get
函式樣版:
void test() {
auto my = example::MyClass("example", 42);
auto [x, y] = my;
std::cout << x << " " << example::get<0>(my) << std::endl;
{
using example::get;
std::cout << y << " " << get<1>(my) << std::endl;
}
}
小結
本節我們花費很長的篇幅介紹如何自訂 Structured Binding。即便只想要綁定二個成員變數,為了處理所有情況,我們必需編寫近百行程式碼。自訂 Structured Binding 有時候反而會讓你的程式碼更為複雜。從我個人的經驗來看,std::tuple
足以應付日常需求。即使 std::tuple
不足以處理你面對的情況,在自訂 Structured Binding 之前仍應深思熟慮。
本節將程式碼拆解為四個步驟。如果需要完整的程式碼,請下載 myclass-member-function-get.cpp 與 myclass-free-function-get.cpp。
其他細節
在 C++ 11 使用 std::tie
綁定回傳值的時候,如果想要忽略特定元素,我們可以使用 std::ignore
:
double x, z;
std::tie(x, std::ignore, z) = std::make_tuple(1.0, 2.0, 3.0);
然而 Structured Binding 沒有與 std::ignore
對應的表示法。即使你不使用該元素,你還是必需提供一個識別字:
auto [x, unused, z] = std::make_tuple(1.0, 2.0, 3.0);
一些編譯器會因為「後面的程式碼」沒有使用該關鍵字而印出警告。如果要忽略那些警告,可以在 Structured Binding 述句之後加上(將下方 unused
識別字代換為你選用的識別字):
(void)unused;
雖然在 C++ 17 我們能以 [[maybe_unused]]
屬性忽略該警告,然而在制定 C++ 17 的時候沒有處理到 Structured Binding。這個問題已經被回報到語言瑕疪報告 CWG#2360。C++ 20 的草稿已經更新文字敘述。在 C++ 20 編譯器只會在所有識別字都沒有被使用時才會印出警告:
// Has warning: g++ -std=c++17 -Wall
// No warning: clang -std=c++17 -Wall
double example_no_warn_partial_usages() {
auto [x, unused] = std::make_tuple(1.0, 2.0);
return x;
}
// Has warning: g++ -std=c++17 -Wall
// No warning: clang -std=c++17 -Wall
void example_no_warn_maybe_unused() {
[[maybe_unused]] auto [unused1, unused2] =
std::make_tuple(1.0, 2.0);
}
// Must see warnings
void example_warn() {
auto [unused1, unused2] = std::make_tuple(1.0, 2.0);
}
參考資料
- Working Draft, Standard for Programming Language C++ (N4713), Section 11.5. Structured binding declarations
- Working Draft, Standard for Programming Language C++ (N4713), Section 23.5.3.6. Tuple helper class
- P0217R3: Proposed wording for structured bindings
- P0305R1: Selection statements with initializer
- P1091R3 Extending structured bindings to be more like variable declarations
- CWG#2360: [[maybe_unused]] and structured bindings
[1] | Free Function(自由函式)指 C++ 程式碼中不是成員函式的函式。典型的 Free Function 包函 main 函式或者 C 標準函式庫的 getenv 函式等等。 |
Note
如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。