std::any 使用方法

本文要介紹 C++ 17 新增的 std::any 類別。它能儲存所有「可以被複製建構(Copy Constructible)」的數值。以下我們會先介紹 std::any 的基本用法,接著介紹實際的使用情境

使用方法

std::any 類別的使用方法分述如下:

  1. 建構物件
  2. 指派運算子
  3. any_cast 函式
  4. has_value 成員函式
  5. reset 成員函式
  6. emplace 成員函式
  7. type 成員函式

建構物件

std::any 類別定義於 <any> 標頭檔。以下是幾種建構 std::any 物件的方法:

#include <any>

int main() {
  std::any a;  // #1
  std::any i(1);  // #2
  std::any c = std::make_any<double>(1.0);  // #3
}
  1. 以「預設建構式(Default Constructor)」建構一個沒有數值的 std::any 物件。
  2. 將數值傳給 std::any 的建構式,使其儲存該數值。
  3. std::make_any<ValueType> 函式建構一個「帶有 ValueType 數值」的 std::any 物件。 std::make_any 的參數會被傳給 ValueType 的建構式。

不論以何種方式建構 std::any 物件,其儲存的數值必須是「可以被複製建構的(Copy Constructible)」。舉例來說,如果我們試著將不能被複製的 std::unique_ptr 物件傳入 std::any 的建構式,我們會得到編譯錯誤:

#include <any>
#include <memory>

int main() {
  std::any a(std::make_unique<int>(5));  // Compilation error.
}

指派運算子

我們能透過指派運算子更換 std::any 儲存的數值。數值的型別也可以隨意更換:

#include <any>

int main() {
  std::any x;
  x = 1;
  x = "hello world";
  x = 42.0;
}

我們也能將一個 std::any 物件指派給另一個 std::any 物件:

#include <any>

int main() {
  std::any x(42);
  std::any y;
  y = x;
}

any_cast 函式

std::any 儲存的數值必須以 std::any_cast 樣版函式讀取。因為我們必須傳入 std::any_cast 的樣版參數,因此我們必須知道 std::any 儲存數值的型別。如果樣版參數的型別與 std::any 儲存數值的型別不同,std::any_cast 會拋出 std::bad_any_cast 例外:

#include <any>
#include <iostream>

int main() {
  std::any x(42);

  std::cout << std::any_cast<int>(x) << std::endl;

  try {
    std::cout << std::any_cast<double>(x) << std::endl;
  } catch (std::bad_any_cast &exc) {
    std::cout << "exception: " << exc.what() << std::endl;
  }
}

如果傳入給 std::any_caststd::any 物件是參考型別,std::any_cast 會回傳儲存數值的複本

如果我們要直接存取 std::any 物件裡面的數值,std::any_cast 的參數型別必須是 std::any *(指標型別)。例如:

#include <any>
#include <iostream>

int main() {
  std::any x(42);

  int &i = *std::any_cast<int>(&x);
  std::cout << i << std::endl;  // Prints 42

  ++i;
  std::cout << std::any_cast<int>(x) << std::endl;  // Prints 43

#if 0
  int &j = std::any_cast<int>(x);  // Compilation error.
  std::cout << j << std::endl;
#endif
}

has_value 成員函式

has_value 成員函式會回傳 std::any 是否擁有數值:

#include <any>
#include <iostream>

int main() {
  std::any x(42);
  std::any y;
  std::cout << "x.has_value(): " << x.has_value() << std::endl;  // 1
  std::cout << "y.has_value(): " << y.has_value() << std::endl;  // 0
}

reset 成員函式

reset 成員函式會清除 std::any 儲存的數值:

#include <any>
#include <iostream>

int main() {
  std::any x(42);
  std::cout << "before: " << x.has_value() << std::endl;  // 1
  x.reset();
  std::cout << "after: " << x.has_value() << std::endl;  // 0
}

emplace 成員函式

emplace<ValueType>(...) 成員函式會在 std::any 物件內建構一個型別為 ValueType 的數值。其中 emplace 的參數會成為 ValueType 建構式的參數。如果 std::any 物件已經擁有一個數值,現有的數值會先被解構。

#include <any>
#include <complex>
#include <iostream>

int main() {
  std::any x(42);
  x.emplace<std::complex<double>>(0, 1);
  std::cout << std::any_cast<std::complex<double>>(x) << std::endl;
}

雖然 emplace<ValueType>(...)operator=(ValueType(...)) 功能相似,但是如果 ValueType 的 Copy Constructor(或 Move Constructor)必須花費較長的執行時間,emplace<ValueType>(...) 能為我們節省從 ValueType 暫時物件複製(或搬移)物件的執行時間。

type 成員函式

type 成員函式會回傳數值型別的「執行期型別資訊(Run-Time Type Information, RTTI)」。我們能透過 type 成員函式與 typeid 關鍵字在執行期選擇合適的 std::any_cast 函式樣版參數。如果 std::any 物件沒有數值,type 成員函式會回傳 typeid(void)

#include <any>
#include <iostream>
#include <typeinfo>

void print(const std::any &x) {
  if (x.type() == typeid(int)) {
    std::cout << "int: " << std::any_cast<int>(x) << std::endl;
  } else if (x.type() == typeid(double)) {
    std::cout << "double: " << std::any_cast<double>(x) << std::endl;
  } else if (x.type() == typeid(void)) {
    std::cout << "no value" << std::endl;
  } else {
    std::cout << "unhandled type: " << x.type().name() << std::endl;
  }
}

int main() {
  print(std::any(42));
  print(std::any(1.0));
  print(std::any());
}

使用情境

在設計 API 的時候,我們時常必須為 Callback 函式保管它們的 Context Object(上下文物件)。之後呼叫 Callback 函式的時候再將 Context Object 傳給 Callback 函式。然而這時候我們會遇到兩個問題:

  1. 保管 Context Object 的一方不想要對 Context Object 施加太多限制、也不想要知道 Context Object 的型別。
  2. 保管 Context Object 的一方必須知道如何解構 Context Object。

std::any 類別能很優雅地解決這兩問題。一個簡單的範例如下:

#include <any>
#include <iostream>
#include <utility>
#include <vector>

// File 1: event_source.cpp
class EventSource {
public:
  using EventListener = void (*)(std::any &);

  void addEventListener(EventListener callback, std::any context) {
    listeners_.push_back(std::make_pair(callback, std::move(context)));
  }

  void dispatchEvent() {
    for (auto &&listener : listeners_) {
      listener.first(listener.second);
    }
  }

private:
  std::vector<std::pair<EventListener, std::any>> listeners_;
};

// File 2: callback1.cpp
void callback1(std::any &context_any) {
  int &ctx = *std::any_cast<int>(&context_any);
  std::cout << "callback1: " << ctx++ << std::endl;
}

void attach1(EventSource &source) {
  source.addEventListener(callback1, std::any(0));
}

// File 3: callback3.cpp
void callback2(std::any &context_any) {
  double &ctx = *std::any_cast<double>(&context_any);
  ctx *= 1.2;
  std::cout << "callback2: " << ctx << std::endl;
}

void attach2(EventSource &source) {
  source.addEventListener(callback2, std::any(1.0));
}

// File 4: main.cpp
int main() {
  EventSource source;
  attach1(source);
  attach2(source);

  source.dispatchEvent();
  source.dispatchEvent();
}

在實際情況下,EventSourcecallback1callback2 會分別屬於不同的 C++ 原始檔。EventSource 不需要知道 Context Object 的實際型別。各個 Callback 在得到 std::any 物件之後,會依據自己的邏輯透過 std::any_cast 取得它們自己的 Context Object。

參考資料