类型安全的任意类型容器 - std::any

1. std::any 概述

在学习 C++ 过程中,你可能遇到过这样的烦恼:C++ 是强类型语言,每个变量都必须有明确的类型(int, double, std::string 等)。如果你想写一个函数,既能存 int,又能存 string,甚至存你自己定义的类对象,通常只能用:

  1. 模板:但模板必须在编译期确定类型。
  2. 空指针 void*:这是 C 语言的做法,不安全,且无法记住类型信息。
  3. 联合体 union:C++ 的 union 有很多限制(比如不能有 std::string 这种构造函数复杂的对象)。

为了解决这个问题,C++17 引入了 std::any它是一种类型安全、可以存储任何可拷贝构造类型的容器。

大家可以把它想象成一个贴了标签的万能快递箱

  • 使用者往里面扔什么东西都可以(int, double, std::vector, 自定义类…)。
  • 它会自动记住我们扔进去的是什么类型(这就是标签)。
  • 当把东西取出来的时候,如果猜的类型不对,它会报错(这就是类型安全),而不是给我们一堆乱码让程序崩溃。

std::any 持有的是值的拷贝(除非存储的是引用包装器),对于小对象,std::any 通常使用内部缓冲区存储,避免动态内存分配。头文件和命名空间:

1
2
#include <any>
using namespace std;
std::any 类 API 在线查询

使用 std::any 的基本操作流程分为三步:存 -> 查 -> 取

  1. 存入数据:我们可以像赋值一样直接存,或者使用 std::make_any 构造。

    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
    #include <any>
    #include <string>
    #include <iostream>
    #include <vector>

    void basic_usage()
    {
    // 1. 默认构造(空对象)
    std::any a1;
    std::cout << "a1 has_value: " << a1.has_value() << std::endl; // 0

    // 2. 直接存储各种类型
    std::any a2 = 42; // int
    std::any a3 = 3.14; // double
    std::any a4 = std::string("Hello"); // std::string
    std::any a5 = "C-string"; // const char*

    // 3. 使用 std::make_any
    auto a6 = std::make_any<std::vector<int>>(3, 100); // vector of 3 elements, each 100
    auto a7 = std::make_any<std::pair<int, std::string>>(1, "apple");
    }

    int main()
    {
    basic_usage();
    return 0;
    }
  2. 检查类型:由于它什么都可以存,取出时必须先确认里面是什么。

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    #include <any>
    #include <string>
    #include <iostream>
    #include <vector>

    void type_information()
    {
    std::any values[] = {
    42,
    3.14,
    std::string("text"),
    std::vector<int>{1, 2, 3}
    };

    for (const auto& val : values)
    {
    if (val.has_value())
    {
    // 使用 type() 获取 type_info
    const std::type_info& ti = val.type();
    // 注意:ti.name() 返回的实现定义的名字,可能不易读
    std::cout << "Type: " << ti.name() << std::endl;

    // 根据类型名进行判断(编译器相关,可能不友好)
    if (ti == typeid(int))
    {
    std::cout << " -> It's an int: " << std::any_cast<int>(val) << std::endl;
    }
    else if (ti == typeid(double))
    {
    std::cout << " -> It's a double: " << std::any_cast<double>(val) << std::endl;
    }
    }
    }
    }

    int main()
    {
    type_information();
    return 0;
    }
  3. 取出数据:取出数据需要使用 std::any_cast,这是最关键的一步。std::any_cast 是从箱子拿东西的唯一工具,但它非常严格,取数据过程中需要遵循以下规则:

    • 类型必须完全匹配:C++ 的类型转换规则在这里不适用。

      • intlong 是不同的类型。
      • intint& 是不同的类型(如果存的是 int,必须用 int 取,不能用 int& 直接取)。
      1
      2
      3
      std::any a = 10;
      // long b = std::any_cast<long>(a); // 抛出异常!虽然 int 和 long 隐式转换,但 std::any 不允许。
      int c = std::any_cast<int>(a); // 正确
    • 使用指针转换避免异常

      如果我们不想用 try-catch,可以使用指针版本的 any_cast。当类型不匹配的时候,它不会抛出异常,而是返回 nullptr

      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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      #include <iostream>
      #include <any>
      #include <string>

      int main()
      {
      // 创建一个 any 对象,存储一个字符串
      std::any data = std::string("Hello, C++17!");

      // 场景1:正确的类型转换 - 指针版本
      // 注意:这里获取的是指针,不是引用!
      std::string* strPtr = std::any_cast<std::string>(&data);
      if (strPtr != nullptr)
      {
      std::cout << "转换成功: " << *strPtr << std::endl; // 需要解引用
      }
      else
      {
      std::cout << "转换失败: nullptr" << std::endl;
      }

      // 场景2:错误的类型转换 - 指针版本
      int* intPtr = std::any_cast<int>(&data);
      if (intPtr != nullptr)
      {
      std::cout << "转换成功: " << *intPtr << std::endl;
      }
      else
      {
      std::cout << "转换失败: 试图将 string 转换为 int" << std::endl;
      }

      // 场景3:修改 any 中的值(通过指针)
      if (strPtr != nullptr)
      {
      *strPtr = "值已被修改";
      std::cout << "修改后的值: " << *std::any_cast<std::string>(&data) << std::endl;
      }

      // 场景4:与引用版本的对比
      try
      {
      // 引用版本:类型不匹配会抛出异常
      int& intRef = std::any_cast<int&>(data);
      std::cout << "这行不会执行" << std::endl;
      }
      catch (const std::bad_any_cast& e)
      {
      std::cout << "引用版本抛异常: " << e.what() << std::endl;
      }

      // 场景5:判断 any 是否存储了特定类型的值
      std::any value = 42;
      // 检查是否存储了 int 类型
      if (auto ptr = std::any_cast<int>(&value))
      {
      std::cout << "value 存储的是 int: " << *ptr << std::endl;
      }

      // 检查是否存储了 string 类型
      if (auto ptr = std::any_cast<std::string>(&value))
      {
      std::cout << "这不会执行,因为 value 不是 string" << std::endl;
      }
      else
      {
      std::cout << "value 不是 string 类型" << std::endl;
      }

      return 0;
      }

2. 应用场景举例

2.1 配置系统

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
#include <any>
#include <string>
#include <unordered_map>

class Config
{
private:
std::unordered_map<std::string, std::any> settings;

public:
template<typename T>
void set(const std::string& key, const T& value)
{
settings[key] = value;
}

template<typename T>
T get(const std::string& key) const
{
auto it = settings.find(key);
if (it == settings.end())
{
throw std::runtime_error("Key not found: " + key);
}
try
{
return std::any_cast<T>(it->second);
}
catch (const std::bad_any_cast&)
{
throw std::runtime_error("Type mismatch for key: " + key);
}
}

template<typename T>
T get_or_default(const std::string& key, const T& default_value)
{
auto it = settings.find(key);
if (it == settings.end())
{
return default_value;
}
try
{
return std::any_cast<T>(it->second);
}
catch (const std::bad_any_cast&)
{
return default_value;
}
}
};

int main()
{
Config config;

// 存储不同类型的配置
config.set("window_width", 800);
config.set("window_height", 600);
config.set("window_title", std::string("My Application"));
config.set("fullscreen", false);
config.set("fps", 60.0);
config.set("resolution", std::pair<int, int>(1920, 1080));

// 读取配置
int width = config.get<int>("window_width");
std::string title = config.get<std::string>("window_title");
double fps = config.get_or_default("fps", 30.0);

std::cout << "Window: " << title << " " << width << "x"
<< config.get<int>("window_height") << std::endl;
std::cout << "FPS: " << fps << std::endl;
return 0;
}

2.2 类型安全的异构容器

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <iostream>
#include <any>
#include <string>
#include <vector>

class HeterogeneousContainer
{
private:
std::vector<std::any> items;

public:
template<typename T>
void add(T&& value)
{
items.emplace_back(std::forward<T>(value));
}

template<typename T>
std::vector<T*> get_all_of_type()
{
std::vector<T*> result;
for (auto& item : items)
{
if (auto* ptr = std::any_cast<T>(&item))
{
result.push_back(ptr);
}
}
return result;
}

void process_all()
{
for (size_t i = 0; i < items.size(); ++i)
{
const auto& item = items[i];
const std::type_info& ti = item.type();

std::cout << "Item " << i << ": type = " << ti.name() << ", value = ";

// 根据类型处理(需要预先知道可能的类型)
if (ti == typeid(int))
{
std::cout << std::any_cast<int>(item);
}
else if (ti == typeid(double))
{
std::cout << std::any_cast<double>(item);
}
else if (ti == typeid(std::string))
{
std::cout << std::any_cast<std::string>(item);
}
else
{
std::cout << "[unknown type]";
}

std::cout << std::endl;
}
}
};

int main()
{
HeterogeneousContainer container;

container.add(10);
container.add(3.14);
container.add(std::string("Hello"));
container.add(20);
container.add(std::string("World"));
container.add(2.718);

// 获取所有整数
auto ints = container.get_all_of_type<int>();
std::cout << "Integers: ";
for (int* ptr : ints)
{
std::cout << *ptr << " ";
}
std::cout << std::endl;

// 处理所有元素
container.process_all();
return 0;
}

3. 性能与注意事项

std::any 并不是零开销的抽象。

  1. 内存开销std::any 通常需要占用额外的内存来存储类型信息和对象本身(通常至少 2 个指针的大小,或者更多,取决于实现和对象大小)。如果对象很小,可能会直接存在栈上;如果对象很大(例如超过 sizeof(void*)*3),它会自动在堆上分配内存。
  2. 运行时开销:每次存取都可能涉及 new/delete(对于大对象)以及类型检查。它比模板慢得多。

因为 std::any 内部管理的是值,所以我们存入的类型必须支持拷贝构造(或者至少支持移动构造并在移动后被标记为有效)。

1
2
// UniquePtr 只能移动,不能拷贝
std::any a = std::unique_ptr<int>(new int(10)); // 编译错误!

在 C++ 中std::anystd::variantvoid*都可用存储任意类型,那么三者有什么不同呢?

  • std::any所有的类型都是未知的。当我们不知道会有多少种类型,或者类型是运行时动态决定时使用。
  • std::variant所有的类型都是已知的集合。比如,只能是intfloatstringvariantany 快,因为它是基于栈的(或者优化过的),并且不需要动态类型检查。
  • void*:C 风格,不安全,无法自动调用析构函数。现代 C++ 请用 std::any 替代。

std::any 是 C++17 提供的类型安全的通用容器,最后我们再总结一下关于它的使用:

  • 创建:直接赋值或使用 std::make_any
  • 查询:使用 .type() 获取 typeid 并与 typeid(T) 比较,或使用 .has_value()
  • 获取:使用std::any_cast
    • any_cast(a):拷贝取出或引用取出(类型不对抛异常)。
    • any_cast(&a):指针取出(类型不对返回 nullptr)。

std::any 适合逻辑灵活、非极致性能要求的场景。如果类型集合是固定的,优先考虑 std::variant;追求极致性能,请使用模板。