CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • C++17 详解 说明
  • 第一部分——语言特性
  • 1. 快速入门
  • 2. 移除或修正的语言特性
  • 3. 语言澄清(Language Clarification)
  • 4. 通用语言特性
  • 5. 模板(Templates)
  • 6. 代码标注
  • 第二部分 - 标准库的变化
  • 7. std::optional
  • 8. std::variant
  • 9. std::any
    • 9. std::any
      • 基础
      • 使用场景
      • std::any的创建
      • 原位构造
      • 复杂类型
      • 修改值
      • 对象生命周期
      • 访问存储的值
      • 性能与内存考量
      • 从 boost::any 迁移
      • std::any 的示例
      • 总结
      • 编译器支持
  • 10. std::string_view
  • 11. 字符串转换
  • 12. 搜索器与字符串匹配
  • 13. 文件系统
  • 14. 并行STL算法
  • 15. 标准库中的其他变化
  • 16. 移除和弃用的库特性
  • 第三部分 - 更多示例和用例
  • 17. 使用std::optional和std::variant进行重构
  • 18. 使用[[nodiscard]]强制执行代码契约
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
  • 20. 如何实现CSV读取器的并行化
目录

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::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"};
1
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"}};
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::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
);
1
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;
};
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

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

  • 属性:将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;
1
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
上次更新: 2025/04/01, 13:21:34
8. std::variant
10. std::string_view

← 8. std::variant 10. std::string_view→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式