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;
};
1
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 ';
}
1
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
1
2
3
4
5
6

上述示例展示了以下几点:

  • std::any不像std::optionalstd::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"};
1
2
3
4
5
6
7
8
9
10
11

# 原位构造

遵循std::optionalstd::variant的风格,std::any可以使用std::in_place_type高效地进行原位对象创建。

# 复杂类型

在下面的示例中,会需要一个临时对象:

std::any a{UserName{"hello"}};
1

但是使用:

std::any a{std::in_place_type<UserName>, "hello"};
1

对象会使用给定的参数原位创建。

为了方便使用,std::any有一个名为std::make_any的工厂函数,返回:

return std::any(std::in_place_type<T>, std::forward<Args>(args)...);
1

在前面的示例中,我们也可以这样写:

auto a = std::make_any<UserName>{"hello"};
1

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);
1
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 ';
1
2
3

如果在构造函数和析构函数中添加打印语句,我们会得到以下输出:

MyType::MyType
MyType::~MyType
100
1
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;
1
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();
    }
}
1
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::variantstd::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
);
1
2
3
4
5
6

这里的技巧在于,值以各种形式存储在wParamlParam中。有时你只需要使用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;
};
1
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);
1
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;
}
}
1
2
3
4
5
6
7
8
9
10

当然,你必须定义值是如何指定的(消息值的类型是什么),但现在你可以使用真实的类型,而不是对整数进行各种技巧性操作。

struct property {
    property();
    property(const std::string &, const std::any &);
    std::string name;
    std::any value;
};
typedef std::vector<property> properties;
1
2
3
4
5
6
7

properties对象看起来功能很强大,因为它可以存储许多不同的类型。游戏编辑器就是一个可以利用这种结构的示例。

# 总结

关于std::any,需要记住以下几点:

  • std::any不是模板类。
  • std::any使用小缓冲区优化,所以对于像intdouble等简单类型,它不会动态分配内存,但对于较大的类型,它会使用额外的new操作符。
  • std::any可能被认为比较“重量级”,但它提供了很大的灵活性和类型安全性。
  • 你可以使用any_cast访问当前存储的值,any_cast有几种“模式”,例如它可能会抛出异常或者返回nullptr
  • 当你不知道可能的类型时使用它,在其他情况下可以考虑std::variant

# 编译器支持

特性 GCC Clang MSVC
std::any 7.1 4.0 VS 2017 15.0
上次更新: 2025/04/01, 13:21:34

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>