本文要介紹 C++ 17 新增的 std::any 類別。它能儲存所有「可以被複製建構(Copy Constructible)」的數值。以下我們會先介紹 std::any 的基本用法,接著介紹實際的使用情境。
使用方法
std::any 類別的使用方法分述如下:
建構物件
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
}
- 以「預設建構式(Default Constructor)」建構一個沒有數值的
std::any物件。 - 將數值傳給
std::any的建構式,使其儲存該數值。 - 以
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_cast 的 std::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 函式。然而這時候我們會遇到兩個問題:
- 保管 Context Object 的一方不想要對 Context Object 施加太多限制、也不想要知道 Context Object 的型別。
- 保管 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();
}
在實際情況下,EventSource、callback1 與 callback2 會分別屬於不同的 C++ 原始檔。EventSource 不需要知道 Context Object 的實際型別。各個 Callback 在得到 std::any 物件之後,會依據自己的邏輯透過 std::any_cast 取得它們自己的 Context Object。
參考資料
- cppreference.com, std::any





