讓我們來做做這個簡單的小例題:
給定 $N$ 組區間 $[l_i, r_i]$,請找出交集最大的兩個區間。
這題沒有什麼演算法,就是單純窮舉 $O(N^2)$ 組區間對計算交集就對了。不過要計算兩個區間的交集會怎麼寫呢?如果我們用 std::pair<int, int>
來存區間們的話,就會變成:
int intersection(pair<int, int> a, pair<int, int> b) {
return max(0, min(a.second, b.second) - max(a.first, b.first));
}
在比競程時,如果是 pair
愛好者,這些 first
和 second
其實會很常被打出來。又因為這兩個單字的長度有點長,所以許多競賽選手都會在他們的 default code 加上:
#define f first
#define s second
等諸如此類的 macro。不過,這樣其實存在一些缺點:
f
和 first
,這樣會直接 CE。pair
描述的物件是區間、平面上的二維點這種物件時,使用 f
和 s
其實相對不直覺,也許在這時候我們會比較喜歡使用 l, r
或 x, y
。因此,如果有一些替代方法來讓我們省去打一堆 first
和 second
的時間就太好了——這時候就要搬出我們的 Structured binding!請直接看看以下範例:
int intersection(pair<int, int> a, pair<int, int> b) {
auto [al, ar] = a;
auto [bl, br] = b;
return max(0, min(ar, br) - max(al, bl));
}
沒錯,一段語法 auto [name1, name2] = pair
就可以把 pair
內的兩個變數分別提取出來放到 name1
和 name2
兩個變數中!也因此我們能自由的取新的名字來撰寫相關程式。
雖然這段程式碼看起來比前面長了一點,但首先在可讀性上就大幅的增加了,而且可想而知,如果未來我們要設計的函式需要大量呼叫 first
和 second
時,使用 Structured binding 也會大幅減少我們打字的時間。
Structured binding 在 C++17 後才正式成為 C++ 的標準,因此在更低的版本有可能會不適用。不過如果不幸遇到很舊的 C++ 版本,其實可以找時間測看看競賽使用的編譯器認不認得這個功能,有時候即使版本不對,編譯器看得懂的話只會跳警告但還是會幫你編譯完成。
儘管實務上不太建議,但競程中如果因為舊版本綁手綁腳就有點太虧了,所以機器測試是很重要的!
也許有讀者會發現一個問題:如果我們要對 pair
內部的物件做修改的話,不就還是得呼叫那個變數本身的 first
和 second
嗎?
實際上還是可以使用 Structured binding 的,其方法就是使用我們之前學過用來取「別名」的 reference:
auto &[l, r] = itv;
像上面這樣一樣加個 &
在前面,l, r
就會分別對應到 itv
內兩個元素的「本體」,直接對 l, r
進行修改就會同時改到 itv
內部的元素!
也因此,如果你的 pair
是形如 pair<vector<int>, vector<int>>
這種龐大的型別,那麼像上面這樣寫也會讓你的時間複雜度是 $O(1)$,能夠放心使用。
同時,既然能加 reference,在前面加上 const
當然也是可以的:
const auto &[l, r] = itv;
這樣就可以在 $O(1)$ 時間拿出兩個元素,且鎖定在唯獨狀態,某些情況下這會是寫程式的好習慣。
有了前面的和 reference 的結合,我們在撰寫最一開始提到的「區間交集」這道題目時,其輸入就可以這樣寫:
int n;
cin >> n;
vector<pair<int, int>> itv(n);
for (auto &[l, r] : itv)
cin >> l >> r;
與我們介紹 Range-based for loop 時一樣,我們可以直接在宣告變數型別的時候使用 Structured binding,就可以持續省下打出 first
和 second
的時間。
當然,因為這樣的用法不僅限於輸入,所以平常遍歷 pair
陣列時也可以這樣寫:
for (auto [l, r] : itv) {
// do something
}
退一步來說,如果需要索引值,由於 Range-based for loop 在 C++17 以前不支援變數宣告就會有些困擾,但還是能這樣寫:
for (int i = 0; i < n; ++i) {
auto [l, r] = itv[i];
// do something
}
所以只要運氣好的話,整份 code 都不見得需要看見 first
和 second
!
這一小段的內容需要稍微了解過後面文章才會學到的 std::map
,若讀者有興趣可以先去看看 基礎資料結構 / Set 與 Map。
如果寫出了以下的迴圈來遍歷 map
:
map<int, int> mp;
// insert something into mp
for (auto &[key, value] : mp) {
// do something
}
這時候,想像中 key
跟 value
會是 mp
內部每一個 pair
前後兩個元素的 reference,但在 map
中,作為鍵值的第一個元素其實是不可修改的。
而這時候,即使我們沒有加上 const
這個 specifier,Structured binding 還是會自動幫我們繼承他 const
的屬性,來讓我們得到的 key
是唯獨的。
同樣的道理其實也適用於要提取的變數自帶 const
或 reference 等屬性,這裡讀者可以想像是 Structured binding 會自動保留這些屬性就好。
不過反過來說,如果要提取的變數們本身都沒有 const
,我們卻只想讓其中一個提取的變數有 const
屬性的話,這就是 Structured binding 辦不到的了——這是因為這些 specifier 必須加在中括號前面,也就只能一口氣影響所有要提取的變數。
前面我們都只有舉過 pair
當例子,甚至 pair
內的兩個型別都是一樣的,Structured binding 當然沒有這麼遜只能支援這樣的型別。
首先,對於型別 pair<A, B>
,A
和 B
的型別不一樣是完全可以的,例如將 pair<int, double>
提取出來後,就會確實的分別得到 int
和 double
兩個型別的變數。
再來,不是只有 pair
適用於這個語法,實際上在 C++ 的規範中,有三種類型可以被支援:
struct
或 class
描述的 data members上面的描述其實省略了一些特殊狀況,不過在競程中通常不會遇到,因此省略之。
讓我們來一一介紹他們。
對於宣告好的一條陣列,只要使用相同個數的元素,就可以對他使用 Structured binding 來提取出裡面的元素,例如:
int arr[5] = {1, 2, 3, 4, 5};
auto [a, b, c, d, e] = arr;
上面這段語法,a, b, c, d, e
就會分別對應到 arr
的五個元素。
不過用在陣列上,就得打出相應數量的元素量,所以更多時候只適用於短的陣列。
C++ 預計從 C++26 後開始支援將部分元素「打包」成一個 pack 的 Structured binding,來放寬這種每次都得打出對應元素量的限制。
就像 std::pair
一樣,這種把多種型別打包在一起的組合型別就是我們這裡稱呼的 tuple-like 型別,他們的共同點就是有 std::get
這個功能可以提取單一變數中特定位置型別的功能。不過其實只要大於兩種型別,在 C++ 中就會必須得使用 std::tuple
。
因此,與 pair
一樣,tuple
也可以使用 Structured binding:
tuple<int, double, bool> tup = {1, 0.5, true};
auto [a, b, c] = tup;
這樣 a, b, c
三個變數就會對應到 tup
所包含的三個不同變數型別。概念與 pair
基本上是一樣的,就不再多解釋。
相信一定也有一派的選手不喜歡使用 pair
或 tuple
,而是會自己果斷的實作一個 struct
或 class
來打包一坨變數,例如:
struct my_struct {
int x = 1, y = 2;
vector<int> v = {3, 4, 5};
};
即使你設計的 struct
包含若干種不同型別的變數,使用 Structured binding
my_struct S;
auto [x, y, z] = S;
還是能正確的按照順序提取內部的變數!
所以,千萬別以為 Structured binding 是什麼只適用於 pair
的語法,他的用途可多了。
前面我們有提過陣列也能使用 Structured binding,但如果對 std::array
或 std::vector
使用,會發生什麼事呢?直接看看下面兩種範例:
array<int, 3> arr1 = {1, 2, 3};
vector<int> arr2 = {4, 5, 6};
auto [a, b, c] = arr1;
auto [d, e, f] = arr2; // compile error
實際測試的結果會發現,第三行的編譯是會過的,第四行則不會。
其實不會過反而比較合邏輯,這是因為我們通常會把這兩個東西當成一個單一的 class
,而想像中他們的內容物應該已經被打包成一個變數了,所以用 Structured binding 時,元素的個數應該得是一個才會編譯成功。就好像:
struct array_struct {
int a[2] = {0, 1};
};
上面這個 array_struct
其實要寫成 auto [a] = array_struct{};
編譯才會成功,因為 Structured binding 按照這個邏輯只能提取 int a[]
這個型別的變數而已。
不過為什麼 std::array
卻是用元素個數來提取呢?其理由是因為,std::array
支援 std::get
等 tuple-like 的操作,所以在這裡,Structured binding 才把他當成了 tuple-like 的類型來提取。這算是一個比較不直觀的個案。
如果出現 pair<pair<int, int>, pair<int, int>>
這種型別的變數,有辦法用 Structured binding 一口氣提取四個變數嗎?
答案是不行,雖然有點可惜,但目前就是這樣,我們可以期待 C++ 未來做出有可能可以處理這種狀況的改版 :)
在本文章中我們學到了 Structured binding 的用法和用途,Structured binding 在競程上時常會是省下 coding 時間、增加可讀性的好工具,讀者可以多加利用。
而且 Structured binding 還不只適用於 pair
,不管是陣列還是自定義的 struct
或 class
,只要元素數量吻合都是可以做使用的。若覺得記憶起來麻煩的話,讀者可以在使用 Structured binding 時試圖以「如果我是編譯器的話,我看得懂嗎?」這樣的方式來做思考。
當然,由於本文只介紹在競程中常見的用途,實際上 Structured binding 還有更多細節是在這種小篇幅不好解釋的,如果有興趣的讀者,可以自行去 cppreference 參考。