這篇來講一下,在 C++17 的時候,加入的一個還算滿好用的功能、structured binding declaration(文件)。
Structured binding declaration 在 Heresy 來看,主要的目的是讓開發者可以快速地將擁有多項資料的物件中拿出來使用。
像是以 std::tuple<> 來說,如果要把每一筆資料都拿出來的話,標準的作法是透過 std::get<>() 來一個一個拿,或是透過 std::tie() 來做一定程度的簡化。
而如果透過 structured binding declaration 的話,則可以進一步簡化成:
#include <iostream> #include <tuple> #include <string> std::tuple<int, float, std::string> get() { return std::make_tuple(1, 2.0f, "Hello"); } int main(int argc, char** argv) { auto [i, f, s] = get(); // i = 1 -> int // f = 2.0f -> float // s = "Hello" -> std::string return 0; }
這樣一來,i 就會對應到 get() 回傳的 tuple 裡的第一項、型別是 int、值則是 1;後面的 f 和 s 也依此類推。
使用上的限制呢,就是數量要一致了。
而如果是用本來的 std::tie() 來寫的話,就得先宣告出三個變數了。
int i; float f; std::string s; std::tie(i, f, s) = get();
所以這邊可以看的出來,相較於 std::tie(), 透過 structured binding declaration 來寫的話,是又利用了 auto 來進一步簡化了個別型別的宣告、來讓程式寫起來更簡單。
Structured binding declaration 也可以宣告成參考的形式,寫法會是:
auto t = get(); auto& [i, f, s] = t;
這樣一來,i、f、s 就會是 t 這個 tuple 裡面各項的參考了。
而除了拿來和 tuple 搭配使用外,他也可以用在固定大小的陣列、std::pair<> 等其他 tuple-like 的結構(後面再解釋)上,甚至也可以支援自己定義的型別,所以使用上算是滿廣泛的。
搭配陣列使用
如果是用在陣列上,就會是類似下面的樣子:
float vec[3]{ 1.0f,0.0f,0.0f }; auto& [x, y, z] = vec;
這樣就可以把 x、y、z 對應到陣列 vec 的三個數值了。
他同樣也可以用在 std::array<> 這種在編譯階段就確定大小的陣列,但是相對地、它不能用在編譯階段無法確認大小的 std::vector<> 或透過 new 來產生的陣列(float*)上。
搭配 std::pair 使用
而能支援 std::pair<> 也會讓我們在去掃 std::map<> 的時候,可以變得再稍微方便一點。
本來要掃整個 std::map<> 的話,大概會寫成:
std::map<int, std::string> mData{ {1, "Hello"}, {2, "World"} }; for (const auto& item : mData) std::cout << item.first << " : " << item.second << "\n";
每一項都得透過 std::pair<> 的 first 和 second 來存取。
透過 structured binding declaration 的話,則可以直接在 range-base for 裡面就把 first 和 second 取出來、並給予他們有意義的變數名稱:
std::map<int, std::string> mData{ {1, "Hello"}, {2, "World"} }; for (const auto& [key, val] : mData) std::cout << key << " : " << val << "\n";
個人是覺得,這樣寫對之後要維護,應該也算是會更好閱讀的。
不過對於不喜歡用 auto、覺得不好看出型別的人來說,可能會覺得不好看吧?
(Visual Studio 2022 17.2 對 auto 的型別提示功能倒是滿方便的。)
搭配自定義的型別
另外,讓個人覺得比較訝異的,是對於符合某些限制下的自定義型別、結構,也是可以直接支援的。
像是下面這樣簡單的結構,就可以直接搭配 structured binding declaration 來使用。
#include <iostream> struct SData { int i; float f; }; int main(int argc, char** argv) { SData a; auto& [i, f] = a; return 0; }
但是相對地,如果使用 class、裡面有不能直接存取的非 public data member 的話,就會不能直接使用了。
class CData { public: int i; float f; protected: double x; };
像是上面的 CData,因為有一個不能直接從外部存取的 x,所以就不能直接使用 structured binding declaration 了。
而針對這種不能直接使用的型別,其實也還是可以透過自己把它包裝成符合 tuple-like 的狀態,讓他可以支援的!
要做到這件事可以參考《Did you know that C++17 structured bindings support to custom classes can be added?》這篇;基本上就是要針對 CData 這個型別,去定義:
- 代表可以提供存取數量的 std::tuple_size<CData>
- 針對每一個成員透過 std::tuple_element<Idx,CData>::type 定義型別
- 定義 get<Idx>() 函式來存取個別成員
下面就是一個範例:
#include <iostream> class CData { public: int i = 1; float f =2; protected: double x; }; template <size_t N> auto& get(CData& rData) { if constexpr (N == 0) return rData.i; else if constexpr (N == 1) return rData.f; }; namespace std { template <> struct tuple_size<CData> : integral_constant<std::size_t, 2> {}; template <size_t N> struct tuple_element<N, CData> { using type = decltype(get<N>(std::declval<CData&>()));; }; } int main(int argc, char** argv) { CData a; auto& [i, f] = a; return 0; }
而如果有可以透過函式來存取的 private / protected data member,也還是可以透過修改 get<>() 函式來支援。
不過雖然透過這樣的方法,的確可以讓自己的類別支援 structured binding declaration;但是個人是覺得,要使用的時候感覺會不容易判斷取得的變數的內容以及順序。
所以到底要不要這樣用?可能要在評估看看了。