本文要介紹 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