這篇來稍微紀錄一下,在 C++17 裡面新增、可以用來儲存所有可以複製的資料的型別、std::any
(C++ Reference)。
std::any
不是一個 template 型別,在賦予它值的時候,他會去紀錄型別、以及實際的資料;之後可以透過他的 type()
這個函式來知道裡面的資料型別、並可以透過 any_cast<>()
轉型回來。
下面是一個簡單的例子:
#include <any> #include <string> #include <iostream> int main() { std::any a = 1; std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n'; a = 3.14; std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n'; std::string sText = "test"; a = sText; std::cout << a.type().name() << ": " << std::any_cast<std::string>(a) << '\n'; }
上面的程式執行的結果會是類似下面的樣子(這邊是用 MSVC 的結果,g++ 或 clang 要有比較類似的結果可以參考《在 C++ 顯示完整的 typeid 型別名稱》):
int: 1 double: 3.14 class std::basic_string,class std::allocator >: test
這邊是建立了一個名為 a
的 std::any
物件,一開始是賦予他一個整數 1、所以透過 type()
取得的 type_info
會是 int
;而如果要取得他的值來用的話,則是要透過 any_cast<int>()
、明確地指定型別來取得。
之後則是用 3.14 這個浮點數來重新指定 a
的值,所以之後 type()
就會是 double
。
而這邊也可以用比較複雜的型別、像是 std::string
來做同樣的操作。
這邊的寫法是先建立一個 std::string
的物件、再把他指派給 a
;實際上,這邊也可以搭配 std::make_any<>()
或是 std::any
的 emplace<>()
這個成員函式來使用:
a = std::make_any<std::string>("test"); a.emplace<std::string>("test");
這兩種寫法基本上可以透過指定型別、建構的參數、直接使用特定的建構子來產生 std::any
物件;理論上可以減少一個複製的動作、效率會比較好。
另外,在呼叫 std::any_cast<std::string>(a)
的時候,實際上是會把 a
內儲存的字串複製一份傳出來;如果不想要複製的話,也可以用參考的形式來取得內部的字串、甚至也可以直接修改他的內容:
std::any a = std::make_any<std::string>("test"); std::cout << std::any_cast<std::string>(a) << '\n'; // will make a copy std::string& s = std::any_cast<std::string&>(a); // reference s = "hello world"; // modify the data in a std::cout << std::any_cast<std::string const&>(a) << '\n'; // const reference
在存取的時候,如果非常在意效能、想要避免無謂的複製的話,可能採用參考的形式來存取會比較好。
基本上,應該是只要是有複製建構子(copy constructor)的型別,都可以用 std::any
來做儲存;所以如果是自己定義的類別想要用 std::any
來儲存或傳遞的話,只要確認有複製建構子就可以了。
下面是個簡單的例子:
#include <any> #include <iostream> class CData { public: CData(int x) :val(x) {} CData(const CData&) = default; public: int val; }; int main() { std::any a = CData(1); std::cout << a.type().name() << ": " << std::any_cast<CData>(a).val << '\n'; }
實際上,由於 C++ 在可能的狀況下是會自己產生 copy constructor 的,所以其實上面的「CData(const CData&) = default;
」其實在這個例子是可以不用加的;不過這邊如果把它改成:
CData(const CData&) = delete;
像這樣明確地告訴編譯器這個類別不要產生 copy constructor 的話,那 CData
之後就不能搭配 std::any
使用了~
另外,std::any
其實有允許沒有值的狀態,可以透過 has_value()
來確認;同時,也可以透過 reset()
這個函式來把內部的值清空。
下面就是簡單的使用示意:
#include <any> #include <iostream> int main() { std::cout << std::boolalpha; std::any a; std::cout << "have value: " << a.has_value() << "\n"; // false a = 1; std::cout << "have value: " << a.has_value() << "\n"; // true a.reset(); std::cout << "have value: " << a.has_value() << "\n"; // false }
而如果在使用 any_cast<>()
的時候出現錯誤的話,則是會丟出 std::bad_any_cast
這個例外;在可能出錯想攔截錯誤的時候,可以透過 try-catch 來處理:
std::any a = 1; try { std::cout << std::any_cast<float>(a) << '\n'; } catch (const std::bad_any_cast& e) { std::cerr << e.what() << '\n'; }
至於什麼時候會需要用到 std::any
呢?
如果是想透過 std::vector<std::any>
這類的形式來儲存並管理不同類型的資料基本上不是不行,但是由於要存取的時候是需要明確的型別轉換的,所以感覺上好像並不會比較方便?
如果真的要的話,大概就是在要存取的時候、得根據 type()
取得的 std::type_info
來做窮舉,如果真有這種需要的時候倒也不是不能用。下面是一個例子:
#include <any> #include <vector> #include <string> #include <iostream> int main(int argc, char** argv) { std::vector<std::any> vObjects; vObjects.push_back(std::make_any<int>(1)); vObjects.push_back(std::make_any<float>(1.0f)); vObjects.push_back(std::make_any<double>(1.0)); vObjects.push_back(std::make_any<std::string>("test")); for (const auto& a : vObjects) { const std::type_info& t = a.type(); if (t == typeid(int)) { std::cout << "INT " << std::any_cast<int>(a) << "\n"; } else if (t == typeid(float)) { std::cout << "FLOAT " << std::any_cast<float>(a) << "\n"; } else if (t == typeid(std::string)) { std::cout << "STRING " << std::any_cast<std::string>(a) << "\n"; } else { std::cout << "Unhandled type: " << t.name() << "\n"; } } }
不過,如果是已經知道有哪些可能的型別、而且有辦法窮舉的話,感覺上用 std::variant
應該會更為合適?
真要說的,個人會覺得他或許可以在設計 API 的時候、用來取代 void*
、來作為傳遞額外資料的型別?
很多 C/C++ 的函式庫在 callback function 要傳遞額外的資料的時候,由於沒辦法確定使用者需要傳遞的型別,所以常常會是直接用 void*
、讓使用者傳遞任何型別的指標、然後要使用的時候自己再去強制轉型。
像是以前介紹過的 NiTE 的 callback 都有留一個 void* pCookie
、讓使用者可以傳遞自己需要的資料(參考);而實際上這樣的設計也算是滿常見的,像是 FLTK(官網)的 callback 大多也都有留一個 void* data
讓使用者自行發揮。
理論上在這邊應該是可以透過 std::any
來取代 void*
、提供一個非指標、type-safe 的方式來傳遞任何型別的物件了?但是真的要這樣設計到底好不好其實就不曉得了。 XD