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

类型安全的任意类型容器 - std::any
苏丙榅1. std::any 概述
在学习 C++ 过程中,你可能遇到过这样的烦恼:C++ 是强类型语言,每个变量都必须有明确的类型(int, double, std::string 等)。如果你想写一个函数,既能存 int,又能存 string,甚至存你自己定义的类对象,通常只能用:
- 模板:但模板必须在编译期确定类型。
- 空指针
void*:这是 C 语言的做法,不安全,且无法记住类型信息。 - 联合体
union:C++ 的 union 有很多限制(比如不能有std::string这种构造函数复杂的对象)。
为了解决这个问题,C++17 引入了 std::any,它是一种类型安全、可以存储任何可拷贝构造类型的容器。
大家可以把它想象成一个贴了标签的万能快递箱:
- 使用者往里面扔什么东西都可以(
int,double,std::vector, 自定义类…)。 - 它会自动记住我们扔进去的是什么类型(这就是标签)。
- 当把东西取出来的时候,如果猜的类型不对,它会报错(这就是类型安全),而不是给我们一堆乱码让程序崩溃。
std::any 持有的是值的拷贝(除非存储的是引用包装器),对于小对象,std::any 通常使用内部缓冲区存储,避免动态内存分配。头文件和命名空间:
1 |
|
使用 std::any 的基本操作流程分为三步:存 -> 查 -> 取。
存入数据:我们可以像赋值一样直接存,或者使用
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
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;
}检查类型:由于它什么都可以存,取出时必须先确认里面是什么。
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
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;
}取出数据:取出数据需要使用
std::any_cast,这是最关键的一步。std::any_cast是从箱子拿东西的唯一工具,但它非常严格,取数据过程中需要遵循以下规则:类型必须完全匹配:C++ 的类型转换规则在这里不适用。
int和long是不同的类型。int和int&是不同的类型(如果存的是int,必须用int取,不能用int&直接取)。
1
2
3std::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
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.2 类型安全的异构容器
1 |
|
3. 性能与注意事项
std::any 并不是零开销的抽象。
- 内存开销:
std::any通常需要占用额外的内存来存储类型信息和对象本身(通常至少 2 个指针的大小,或者更多,取决于实现和对象大小)。如果对象很小,可能会直接存在栈上;如果对象很大(例如超过sizeof(void*)*3),它会自动在堆上分配内存。 - 运行时开销:每次存取都可能涉及
new/delete(对于大对象)以及类型检查。它比模板慢得多。
因为 std::any 内部管理的是值,所以我们存入的类型必须支持拷贝构造(或者至少支持移动构造并在移动后被标记为有效)。
1 | // UniquePtr 只能移动,不能拷贝 |
在 C++ 中std::any 、std::variant 、 void*都可用存储任意类型,那么三者有什么不同呢?
std::any:所有的类型都是未知的。当我们不知道会有多少种类型,或者类型是运行时动态决定时使用。std::variant:所有的类型都是已知的集合。比如,只能是int或float或string。variant比any快,因为它是基于栈的(或者优化过的),并且不需要动态类型检查。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;追求极致性能,请使用模板。


















