9. std::any
# 9. std::any
使用std::optional
,你可以表示常规类型的值,或者将其标记为空。使用std::variant
,你可以将几种不同类型的可选值包装到一个实体中。而C++17为我们提供了另一种包装类型:std::any
,它能够以类型安全的方式存储任意类型的数据。
在本章中,你将了解到:
- 为什么
void*
是一种非常不安全的模式; std::any
及其主要用途;std::any
的使用示例及用例;any_cast
以及如何使用它的所有 “模式”。
# 基础
在C++14中,在变量里存储可变类型的方式并不多。当然,你可以使用void*
,但这种方式并不安全。void*
只是一个原始指针,你必须管理整个对象的生命周期,并防止它被错误地转换为其他类型。
理论上,可以用一个带有类型鉴别器的类来包装void*
。
class MyAny {
void* _value;
TypeInfo _typeInfo;
};
2
3
4
如你所见,我们有了这种类型的基本形式,但要确保MyAny
的类型安全性,还需要编写一些代码。这就是为什么最好使用标准库,而不是自己实现一个。
这正是C++17中std::any
的基本形式。它允许你在一个对象中存储任意类型的数据,并且当你试图访问一个非当前存储类型的值时,它会报告错误(或抛出异常)。
一个小示例:
Chapter Any/any_demo.cpp
std::any a(12);
// 设置任意值:
a = std::string("Hello!");
a = 16;
// 读取值:
// 我们可以将其作为int读取
std::cout << std::any_cast<int>(a) << '\n ';
// 但不能作为string读取:
try {
std::cout << std::any_cast<std::string>(a) << '\n ';
}
catch (const std::bad_any_cast& e) {
std::cout << e.what() << '\n ';
}
// 重置并检查它是否包含值:
a.reset();
if (!a.has_value()) {
std::cout << "a is empty!" << '\n ';
}
// 你可以在容器中使用它:
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;
for (auto &[key, val] : m) {
if (val.type() == typeid(int))
std::cout << "int: " << std::any_cast<int>(val) << '\n ';
else if (val.type() == typeid(std::string))
std::cout << "string: " << std::any_cast<std::string>(val) << '\n ';
else if (val.type() == typeid(float))
std::cout << "float: " << std::any_cast<float>(val) << '\n ';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
上述代码的输出结果为:
16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World
2
3
4
5
6
上述示例展示了以下几点:
std::any
不像std::optional
或std::variant
那样是一个模板类。- 默认情况下它不包含值,你可以通过
.has_value()
来检查。 - 你可以通过
.reset()
重置一个any
对象。 - 它作用于 “退化” 类型,所以在赋值、初始化或原位构造之前,类型会通过
std::decay
(opens new window)进行转换。 - 当赋一个不同类型的值时,当前活动类型的对象会被销毁。
- 你可以使用
std::any_cast<T>
来访问值。如果当前活动类型不是T
,它会抛出bad_any_cast
异常。 - 你可以使用
.type()
来获取当前活动类型,该函数返回类型的std::type_info
(opens new window)。
# 使用场景
虽然void*
在某些有限的用例中是一种极其不安全的模式,但std::any
增加了类型安全性,因此它有更多的应用场景。
一些可能的场景:
- 在库中 —— 当库类型必须存储或传递任意数据,却不知道可用的类型集合时;
- 文件解析 —— 如果你确实无法指定受支持的类型;
- 消息传递;
- 与脚本语言的绑定;
- 实现脚本语言的解释器;
- 用户界面 —— 控件可能需要存储任意数据;
- 编辑器中的实体。
在许多情况下,你可以限制支持的类型数量,因此std::variant
可能是更好的选择。当然,当你在实现一个库时,如果不知道最终的应用场景,情况就会变得棘手,因为你不知道对象中将会存储哪些可能的类型。
# std::any的创建
创建std::any
对象有几种方式:
- 默认初始化 —— 此时对象为空;
- 直接用值/对象初始化;
- 使用
std::in_place_type
进行原位构造; - 通过
std::make_any
创建。
你可以在以下示例中看到这些方式:
Chapter Any/any_creation.cpp
// 默认初始化:
std::any a;
assert(!a.has_value());
// 用对象初始化:
std::any a2{10}; // int
std::any a3{MyType{10, 11}};
// 原位构造:
std::any a4{std::in_place_type<MyType>, 10, 11};
std::any a5{std::in_place_type<std::string>, "Hello World"};
// make_any
std::any a6 = std::make_any<std::string>{"Hello World"};
2
3
4
5
6
7
8
9
10
11
# 原位构造
遵循std::optional
和std::variant
的风格,std::any
可以使用std::in_place_type
高效地进行原位对象创建。
# 复杂类型
在下面的示例中,会需要一个临时对象:
std::any a{UserName{"hello"}};
但是使用:
std::any a{std::in_place_type<UserName>, "hello"};
对象会使用给定的参数原位创建。
为了方便使用,std::any
有一个名为std::make_any
的工厂函数,返回:
return std::any(std::in_place_type<T>, std::forward<Args>(args)...);
在前面的示例中,我们也可以这样写:
auto a = std::make_any<UserName>{"hello"};
make_any
可能使用起来更直观。
# 修改值
当你想要修改std::any
中当前存储的值时,有两种选择:使用emplace
或赋值操作:
Chapter Any/any_changing.cpp
std::any a;
a = MyType(10, 11);
a = std::string("Hello");
a.emplace<float>(100.5f);
a.emplace<std::vector<int>>({10, 11, 12, 13});
a.emplace<MyType>(10, 11);
2
3
4
5
6
# 对象生命周期
std::any
保证安全的关键在于不泄漏任何资源。为了实现这一点,std::any
会在赋新值之前销毁当前活动的对象。
Chapter Any/any_lifetime.cpp
std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << '\n ';
2
3
如果在构造函数和析构函数中添加打印语句,我们会得到以下输出:
MyType::MyType
MyType::~MyType
100
2
3
any
对象最初用MyType
初始化,但在赋予新值(100.0f)之前,它会调用MyType
的析构函数。
# 访问存储的值
要访问std::any
中当前存储的值,你只有一种选择:std::any_cast<T>()
。
这个函数有三种“模式”:
- 读取访问:以
std::any
的引用作为参数,返回值的副本,转换失败时抛出std::bad_any_cast
异常。 - 读写访问:以
std::any
的引用作为参数,返回引用,转换失败时抛出std::bad_any_cast
异常。 - 读写访问:以
std::any
的指针作为参数,返回指向值(可以是常量指针或非常量指针)的指针,转换失败时返回nullptr
。
简而言之:
std::any var = 10;
// 读取访问:
auto a = std::any_cast<int>(var);
// 通过引用进行读写访问:
std::any_cast<int&>(var) = 11;
// 通过指针进行读写访问:
int* ptr = std::any_cast<int>(&var);
*ptr = 12;
2
3
4
5
6
7
8
看下面这个例子:
// Any/any_access.cpp
struct MyType {
int a, b;
MyType(int x, int y) : a(x), b(y) {}
void Print() { std::cout << a << ", " << b << '\n'; }
};
int main() {
std::any var = std::make_any<MyType>(10, 10);
try {
std::any_cast<MyType&>(var).Print();
std::any_cast<MyType&>(var).a = 11; // 读写
std::any_cast<MyType&>(var).Print();
std::any_cast<int>(var); // 抛出异常!
} catch (const std::bad_any_cast& e) {
std::cout << e.what() << '\n';
}
int* p = std::any_cast<int>(&var);
std::cout << (p? "包含一个整数... \n " : "不包含整数...\n");
if (MyType* pt = std::any_cast<MyType>(&var); pt) {
pt->a = 12;
std::any_cast<MyType&>(var).Print();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
关于错误处理有两种选择:通过异常(std::bad_any_cast
)或者返回指针(或nullptr
)。std::any_cast
用于指针访问的函数重载也标记了noexcept
。
# 性能与内存考量
std::any
看起来功能十分强大,你可能会用它来存储不同类型的变量……但你可能会问,这种灵活性的代价是什么。
主要问题在于额外的动态内存分配开销。
std::variant
和std::optional
不需要任何额外的内存分配,这是因为它们知道对象中将会存储哪种(或哪些)类型。而std::any
并不知道可能存储的类型,所以它可能会使用一些堆内存。
这种情况是总会发生,还是有时发生呢?有什么规则吗?对于像int
这样的简单类型也会发生吗?
让我们看看标准是怎么说的(23.8.3 [any.class (opens new window)]:
实现应避免为存储的小对象值使用动态分配的内存。例如:当构造的对象仅保存一个int 类型的值时。这种小对象优化仅适用于is_nothrow_move_constructible_v<T> 为true 的类型T 。 |
---|
总之,标准鼓励实现使用小缓冲区优化(Small Buffer Optimisation,SBO)。但这也有一定代价:为了容纳这个缓冲区,类型的大小会增加。
让我们检查一下std::any
的大小:
以下是三个编译器的结果:
编译器 | sizeof(any) |
---|---|
GCC 8.1(Coliru) | 16 |
Clang 7.0.0(Wandbox) | 32 |
MSVC 2017 15.7.0 32位 | 40 |
MSVC 2017 15.7.0 64位 | 64 |
一般来说,如你所见,std::any
不是一个“简单”的类型,它会带来很多开销。由于SBO,它通常不小,在GCC、Clang中占用16或32字节,在MSVC中甚至会占用64字节。
你可以查看Any/any_sizeof.cpp
中的代码。
# 从 boost::any 迁移
Boost Any大约在2001年(1.23.0版本)被引入。有趣的是,Boost库的作者凯夫林·亨尼(Kevlin Henney)也是std::any
提案的作者。所以这两种类型紧密相关,并且STL版本在很大程度上基于其前身。
以下是主要的变化:
特性 | Boost.Any(1.67.0)⁴ | std::any |
---|---|---|
额外内存分配 | 是 | 是 |
小缓冲区优化 | 是 | 是 |
emplace | 否 | 是 |
构造函数中的in_place_type_t | 否 | 是 |
批注:
⁴ 此处版本号仅为示例,实际使用中Boost.Any可能有不同版本
这两种类型之间的差异并不多。大多数情况下,你可以轻松地将boost::any
转换为STL版本。
# std::any 的示例
std::any
的核心在于灵活性。在下面的示例中,你可以看到一些思路,支持变量类型如何能让应用程序变得更简单一些。
- 解析文件:在
std::variant
的示例中,你可以看到如何解析配置文件并将结果存储为几种类型中的一种。如果你编写一个完全通用的解决方案,例如作为某个库的一部分,那么你可能不知道所有可能的类型。从性能角度来看,将std::any
作为属性的值来存储可能就足够了,而且还能提供灵活性。 - 消息传递:在主要为C语言的Windows API中,有一个消息传递系统,它使用消息ID和两个可选参数来存储消息的值。基于这个机制,你可以实现
WndProc
来处理传递给窗口/控件的消息:
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
2
3
4
5
6
这里的技巧在于,值以各种形式存储在wParam
或lParam
中。有时你只需要使用wParam
的几个字节……
如果我们将这个系统改为使用std::any
,以便消息可以将任何内容传递给处理方法,会怎么样呢?
例如:
// Any/any_winapi.cpp
class Message {
public:
enum class Type {
Init,
Closing,
ShowWindow,
DrawWindow
};
public:
explicit Message(Type type, std::any param) :
mType(type), mParam(param)
{ }
explicit Message(Type type) :
mType(type) { }
Type mType;
std::any mParam;
};
class Window {
public:
virtual void HandleMessage(const Message& msg) = 0;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
例如,你可以向窗口发送一条消息:
Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);
2
然后窗口可以用以下消息处理程序来响应这条消息:
switch (msg.mType) {
// ...
case Message::Type::ShowWindow: {
auto pos = std::any_cast<std::pair<int, int>>(msg.mParam);
std::cout << "ShowWindow: "
<< pos.first << ", "
<< pos.second << '\n';
break;
}
}
2
3
4
5
6
7
8
9
10
当然,你必须定义值是如何指定的(消息值的类型是什么),但现在你可以使用真实的类型,而不是对整数进行各种技巧性操作。
- 属性:将
any
引入C++的原始论文N1939 (opens new window)展示了一个属性类的示例。
struct property {
property();
property(const std::string &, const std::any &);
std::string name;
std::any value;
};
typedef std::vector<property> properties;
2
3
4
5
6
7
properties
对象看起来功能很强大,因为它可以存储许多不同的类型。游戏编辑器就是一个可以利用这种结构的示例。
# 总结
关于std::any
,需要记住以下几点:
std::any
不是模板类。std::any
使用小缓冲区优化,所以对于像int
、double
等简单类型,它不会动态分配内存,但对于较大的类型,它会使用额外的new
操作符。std::any
可能被认为比较“重量级”,但它提供了很大的灵活性和类型安全性。- 你可以使用
any_cast
访问当前存储的值,any_cast
有几种“模式”,例如它可能会抛出异常或者返回nullptr
。 - 当你不知道可能的类型时使用它,在其他情况下可以考虑
std::variant
。
# 编译器支持
特性 | GCC | Clang | MSVC |
---|---|---|---|
std::any | 7.1 | 4.0 | VS 2017 15.0 |