C++17 用來儲存任意型別的類別 std::any

| | 0 Comments| 10:52
Categories:

這篇來稍微紀錄一下,在 C++17 裡面新增、可以用來儲存所有可以複製的資料的型別、std::anyC++ 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

這邊是建立了一個名為 astd::any 物件,一開始是賦予他一個整數 1、所以透過 type() 取得的 type_info 會是 int;而如果要取得他的值來用的話,則是要透過 any_cast<int>()、明確地指定型別來取得。

之後則是用 3.14 這個浮點數來重新指定 a 的值,所以之後 type() 就會是 double

而這邊也可以用比較複雜的型別、像是 std::string 來做同樣的操作。

這邊的寫法是先建立一個 std::string 的物件、再把他指派給 a;實際上,這邊也可以搭配 std::make_any<>() 或是 std::anyemplace<>() 這個成員函式來使用:

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

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *