摘要訊息 : 使用 auto 一次性宣告多個不同型別的變數.
0. 前言
在 C++ 11 之後, 若某個函式有多個回傳值, 那麼我們可以使用 std::tuple
. 例如,
#include <tuple>
template <typename T1, typename T2, typename T3, typename T4, typename T5>
constexpr std::tuple<T1, T2, T3, T4, T5> default_values() {
return {T1 {}, T2 {}, T3 {}, T4 {}, T5 {}};
}
如果我們要使用回傳值, 則需要借助 std::get
或者 std::tie
. 但是, 我們都需要額外宣告一個型別為 std::tuple<T1, T2, T3, T4, T5>
甚至五個型別分別為 T1
, T2
, T3
, T4
和 T5
的變數. 如果這裡面存在某個變數對應的型別並非內建型別, 而且預設的建構子十分複雜, 那麼這個建構子可能需要運作兩次 :
#include <tuple>
template <typename T1, typename T2, typename T3, typename T4, typename T5>
constexpr std::tuple<T1, T2, T3, T4, T5> default_values() {
return {T1 {}, T2 {}, T3 {}, T4 {}, T5 {}};
}
T3 v3; // 如果 T3 的建構十分複雜, 那麼此處要浪費時間進行一次無用的建構
v3 = get<2>(default_values<T1, T2, T3, T4, T5>());
為了主要解決這個問題, C++ 17 引入了結構化繫結.
本文的目錄中的標題過長, 可能影響前面章節的閱讀體驗, 故本篇文章的目錄預設為隱藏不展開狀態, 需要閣下手動展開.
更新紀錄 :
- 2022 年 6 月 16 日進行第一次更新和修正.
1. 結構化繫結
如果閣下只是希望簡單了解結構化繫結, 那麼只需要閱讀本節內容即可.
1.1 基本概念
C++ 17 提案 P0144R2《Structured bindings》提出結構化繫結有三種方式 :
[[attribution]] const volatile auto reference-qualifier [variable-list] = {expression}
;[[attribution]] const volatile auto reference-qualifier [variable-list] {expression}
;[[attribution]] const volatile auto reference-qualifier [variable-list](expression)
.
其中, [[attribution]]
是可選的屬性, 使用的頻率極少, 一般可以直接忽略; const
和 volatile
限定都是可選的; reference-qualifier
是參考限定, 包含 &
和 &&
, 也是可選的; variable-list
是變數列表; expression
是表達式.
我們可以看到, 由於變數的型別可能各不相同, 因此使用結構化繫結宣告變數時, 無法指定具體的型別, 必須使用 auto
進行宣告.
1.2 對類別的結構化繫結
現在我們寫一個最簡單的結構化繫結 :
#include <tuple>
#include <string>
std::tuple<int, char, const char *> t {42, 'L', "hello structured binding!"};
auto [a, b, str] {s};
Code 2 相當於
#include <string>
struct {
int a;
char b;
string str;
} s;
int a;
char b;
string str;
a = s.a;
b = s.b;
str = s.str;
結構化繫結中的 auto
不像普通的 auto
, 對於多個型別不同的變數, 只要它被寫在 []
中, 那麼 auto
就會對裡面的變數分別進行推倒, 而不像 auto a {0}, b {1};
這樣要求變數 a
和 b
的型別一定要相同. 另外, 是否帶有 const
, volatile
和參考限定的規則和普通的變數宣告是一樣的. 如果不帶有參考限定, 那麼初始化時會採用複製操作. 因此, 如果某些型別不可進行複製, 那麼結構化繫結就會擲出編碼錯誤.
除了最簡單的應用之外, 還有一個應用比較多的地方是 Range-For, 特別是針對 std::map
. std::map
的 value_type
是一個 std::pair
, 如果我們要使用 Range-For 對 std::map
進行尋訪, 那麼還需要通過 std::pair
的兩個成員 first
和 second
來獲取鍵和值. 現在有了結構化繫結, 就可以直接使用下面的方式 :
#include <map>
#include <string>
int main(int argc, char *argv[]) {
std::map<int, std::string> m;
for(auto &[key, value] : m) {
//...
}
}
對於類別的結構化繫結還有兩個需要注意的地方 : 結構化繫結中的變數數量必須等同於類別中成員的數量; 類別中的所有成員都必須是 public
成員. 這兩個條件任意一個不滿足, 就會導致編碼錯誤.
std::pair
和 std::tuple
本質上就是全部成員都是外部可存取的類別, 因此特別適用於使用結構化繫結. 另外, 對於位元欄位也可以使用結構化繫結 :
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
struct {
int a : 4;
int b : 2;
int c : 2;
} s {6, 0, 1};
auto [size_4_bit, size_2_bit_1, size_2_bit_2] {s};
cout << size_4_bit << endl; // 輸出結果 : 6
cout << size_2_bit_1 << endl; // 輸出結果 : 0
cout << size_2_bit_2 << endl; // 輸出結果 : 1
}
1.3 對陣列的結構化繫結
結構化繫結除了可以用在類別之外, 還可以用在陣列中. 和類別的結構化繫結一樣, 陣列的結構化繫結中的變數數量必須和陣列的大小是一樣的 :
int arr[] {1, 2, 3};
auto [x, y, z] = arr;
auto &[x2, y2, z2] = arr;
auto &[x3, y3, z3] {arr};
auto [x4, y4, z4] {arr}; // Error : cannot initialize an array element of type 'int' with an lvalue of type 'int [3]'
上面產生編碼錯誤的那一行程式碼, 編碼器錯誤地認為我們是需要使用 arr
去初始化一個陣列 [x4, y4, z4]
, 於是擲出了編碼錯誤. 另外, 針對動態記憶體配置的陣列也是無法直接使用結構化繫結的 :
auto arr {new int[3] {1, 2, 3}};
auto &[x, y, z] = arr; // Error : cannot decompose non-class, non-array type 'int *'
2. 進階
2.1 結構化繫結的本質
結構化繫結的本質和 Range-For 其實是類似的, 都是由編碼器自動幫我們生成對應的程式碼. 在結構化繫結中, 編碼器實際上仍然使用 std::get
函式來對類似於 tuple
的物件進行結構化繫結. 這就使得我們自訂的任何型別 T
, 只要多載一個適用於 T
的 get
函式樣板, 再特製化 std::tuple_size
和 std::tuple_element
, 就可以對 T
使用結構化繫結的語法.
Tip :
std::tuple_size
用於查看一個std::tuple
總共帶有幾個變數;std::tuple_element
用於查看一個std::tuple
的某一個變數的型別.
#include <iostream>
#include <tuple>
struct s {};
template <int>
int get(s) {
return 0;
}
namespace std {
template <>
class tuple_size<s> : public std::integral_constant<int, 1> {};
template <>
class tuple_element<0, s> {
public:
using type = int;
};
}
int main(int argc, char *argv[]) {
s a;
auto [x] {a}; // 相當於 auto x {get<0>(a)};
std::cout << x << std::endl; // 輸出結果 : 0
}
在 Code 8 中, 我們打開了名稱空間 std
, 並且對 std::tuple_size
和 std::tuple_element
針對型別 s
進行了特製化. 這一步是不可少的, 否則就會產生編碼錯誤.
2.2 為動態記憶體配置的陣列進行結構化繫結
從第 2.1 節中我們知道了結構化繫結的本質, 這就為我們自訂結構化繫結語法提供了基礎. 在第 1.3 節中我們曾經提到, 動態記憶體配置的陣列是無法直接使用結構化繫結的, 原因便是沒有為其特製化的函式樣板 get
, std::tuple_size
和 std::tuple_element
. 那麼, 如果我們主動編寫相關的程式碼, 就可以使得結構化繫結用於動態記憶體配置的陣列甚至 std::vector
上 :
#include <tuple>
template <int I>
int &get(int *arr) {
return arr[I];
}
namespace std {
template <>
struct tuple_size<int *> : std::integral_constant<int, array_size> {};
template <>
struct tuple_element<0, int *> {
using type = int;
};
template <>
struct tuple_element<1, int *> {
using type = int;
};
template <>
struct tuple_element<2, int *> {
using type = int;
};
template <>
struct tuple_element<3, int *> {
using type = int;
};
}
int main(int argc, char *argv[]) {
auto arr {new int[array_size] {1, 2, 3}};
auto [x1, x2, x3, x4] {arr}; // Error : use of undeclared identifier 'get'
delete[] arr;
}
Code 9-1 會產生編碼錯誤, 因為編碼器在查找 get
這個名稱的過程中會忽略掉外面的函式樣板 get
. 因此, 我們需要建立一個中介者 :
#include <iostream>
#include <tuple>
template <int N>
struct dynamic_array_binder {
int *arr;
operator int *() {
return this->arr;
}
};
template <int I>
int &get(int *arr) {
return arr[I];
}
namespace std {
template <int N>
struct tuple_size<dynamic_array_binder<N>> : std::integral_constant<int, N> {};
template <int I, int N>
struct tuple_element<I, dynamic_array_binder<N>> {
using type = int;
};
}
template <int N>
auto dissect(int *arr) {
return dynamic_array_binder<N> {arr};
}
int main(int argc, char *argv[]) {
auto arr {new int[100] {1, 2, 3}};
auto [x1, x2, x3, x4] = dissect<4>(arr);
std::cout << x1 << std::endl; // 輸出結果 : 1
std::cout << x2 << std::endl; // 輸出結果 : 2
std::cout << x3 << std::endl; // 輸出結果 : 3
std::cout << x4 << std::endl; // 輸出結果 : 0
delete[] arr;
}
Code 9-2 提供了通用解決方案, 通過修改程式碼配接至任何容器. 而且與前面那一份產生編碼錯誤的程式碼不同, 上述程式碼對於具體的元素數量進行自動調整, 而不像之前那一份程式碼那樣需要手動給出 array_size
.
Tip : 此處的名稱查找涉及極其複雜的規則, 我們暫時不講.
3. 雜項
3.1 某些不支援的結構化繫結語法
結構化繫結可以一定程度上簡化程式碼, 但是一切使用 auto
在某些時候可能有些不太方便, 因此可能會有以下程式碼 :
struct {
int a;
int b;
int c;
} s;
int [a, b, c] {s}; // Error : 結構化繫結中不能明確宣告型別, 只能夠使用 auto
auto [a, long b, c] {s}; // Error : 結構化繫結中的變數不支援宣告為指定的型別
auto [a, &b, c] {s}; // Error : 目前結構化繫結只能統一進行參考限定的宣告, 而不能單一對其中的某個變數進行宣告
Code 10 會產生三個編碼錯誤, 目前 C++ 的結構化繫結暫時還不支援這樣做.
std::tie
可以結合 std::ignore
來忽略某些值, 但是在結構化繫結中不支援這樣做 :
#include <tuple>
struct {
int a;
int b;
int c;
} s;
auto [a, std::ignore, c] {s}; // Error : expected ',' or ']' in lambda capture list
從編碼錯誤的提示中, 我們可以看到, 由於 std::ignore
是一個變數, 因此編碼器會將上面這個結構化繫結錯誤地識別為 Lambda 表達式.
3.2 存在非樣板成員函式 get
的情形
結構化繫結的過程中, 編碼器並不是直接去外部的名稱空間中找到合適的 get
, 而是首先查看變數對應的型別中是否存在一個名稱為 get
的成員函式. 如果確實存在這樣的成員函式, 那麼編碼器會優先匹配這個成員函式, 而並非外部的 get
函式. 這就導致類似於 std::shared_ptr
這樣型別對應的物件無法使用結構化繫結, 即時通過第 2.1 節中類似的方法進行實作, 仍然會產生編碼錯誤 :
#include <memory>
struct X : private std::shared_ptr<int> {
std::string fun_payload;
};
template <int N>
std::string &get(X &x) {
if constexpr(N == 0) {
return x.fun_payload;
}
}
namespace std {
template <>
struct tuple_size<X> : std::integral_constant<int, 1> {};
template <>
struct tuple_element<0, X> {
using type = std::string;
};
}
int main(int argc, char *argv[]) {
X x;
auto &[y] = x; // Error : 'get' is a private member of 'std::__1::shared_ptr<int>'
}
因為 std::shared_ptr
中的成員函式 get
本身就不是用於結構化繫結的. 為了解決這個問題, C++ 17 提案 P0961R1《Relaxing the structured bindings customization point finding rules》提出如果某個類別存在一個名稱為 get
的成員函式, 在結構化繫結的過程中, 若且唯若這個 get
是一個函式樣板並且第一個樣板參數為非型別參數, 才將這個成員函式樣板 get
考慮到結構化繫結的函式候選中. 這樣, Code 12 就可以通過編碼了.
3.3 使用友誼關係存取私用成員
在某個類別 T
中宣告友誼函式或者友誼類別, 都可以使得另一個實體可以存取到 T
中私用的成員. 但是對於結構化繫結來說就不能通過這種方式存取私用成員 :
class C {
int i;
friend void f();
void func(const C &rhs) {
auto [x] = rhs; // Error
}
};
class C2 : C {
friend void f();
};
void f() {
C c1;
C2 c2;
auto x {c1.i}; // OK
auto &[y] {c1}; // Error
auto &[z] {c2}; // Error
}
這不太符合友誼宣告本身的意義. 因此, C++ 提案 P0969R0《Allow structured bindings to accessible members》提出解除這種限制, 使得 Code 13 可以通過編碼.
4. 總結
我們可以發現, 結構化繫結可以減少程式碼的複雜性, 提高效能, 而且可以自訂. 不過這個特性也不是非要不可的, 因為在 C++ 17 之前, 我們還是可以通過其它方法避免效能損失. 這樣的語法不能說是不痛不癢, 但是自訂的結構化繫結確實增加了 C++ 的複雜性.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :