跳到主要内容

C++ 17 std::any

介绍

在 C++17 之前,如果我们想要在一个变量中存储任意类型的值,通常会使用 void* 指针或者通过类继承实现多态。但这些方法都有各自的缺点:void* 指针缺乏类型安全,而多态则需要预先定义继承关系。

为了解决这些问题,C++17 引入了 std::any 类型,它允许我们存储任意类型的单个值,而且是类型安全的。std::any 定义在 <any> 头文件中,属于标准库的一部分。

基本用法

创建和赋值

使用 std::any 非常简单,首先需要包含相应的头文件:

cpp
#include <any>
#include <iostream>
#include <string>

int main() {
// 创建一个空的 std::any
std::any a;

// 检查是否有值
std::cout << "a 是否有值? " << (a.has_value() ? "是" : "否") << std::endl;

// 赋值为整数
a = 42;
std::cout << "a 现在存储整数: " << std::any_cast<int>(a) << std::endl;

// 改变类型,赋值为字符串
a = std::string("Hello, std::any!");
std::cout << "a 现在存储字符串: " << std::any_cast<std::string>(a) << std::endl;

return 0;
}

输出:

a 是否有值? 否
a 现在存储整数: 42
a 现在存储字符串: Hello, std::any!

获取存储的值

要从 std::any 对象中获取存储的值,我们使用 std::any_cast 函数。如果尝试以错误的类型获取值,会抛出 std::bad_any_cast 异常:

cpp
#include <any>
#include <iostream>
#include <string>

int main() {
std::any a = 42;

try {
// 正确的类型转换
std::cout << std::any_cast<int>(a) << std::endl;

// 错误的类型转换,会抛出异常
std::cout << std::any_cast<double>(a) << std::endl;
}
catch (const std::bad_any_cast& e) {
std::cout << "类型转换失败: " << e.what() << std::endl;
}

return 0;
}

输出:

42
类型转换失败: bad any_cast

检查存储的类型

std::any 提供了 type() 方法,返回一个 std::type_info 对象,可以用来检查当前存储的值的类型:

cpp
#include <any>
#include <iostream>
#include <string>
#include <typeinfo>

int main() {
std::any a = 42;

std::cout << "a 存储的类型是: " << a.type().name() << std::endl;

a = std::string("Hello");
std::cout << "a 存储的类型是: " << a.type().name() << std::endl;

return 0;
}
备注

type().name() 返回的字符串与编译器相关,可能不是很易读。例如,在 GCC 上,可能会返回像 i(表示 int)或 NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE(表示 std::string)这样的输出。

进阶用法

存储自定义类型

std::any 可以存储任何可复制构造的类型,包括自定义类型:

cpp
#include <any>
#include <iostream>
#include <string>

class Person {
public:
Person(std::string name, int age) : name_(name), age_(age) {}

void print() const {
std::cout << "姓名: " << name_ << ", 年龄: " << age_ << std::endl;
}

private:
std::string name_;
int age_;
};

int main() {
std::any a = Person("张三", 30);

try {
std::any_cast<Person>(a).print();

// 也可以获取引用,避免复制
const Person& person = std::any_cast<const Person&>(a);
person.print();
}
catch (const std::bad_any_cast& e) {
std::cout << "类型转换失败: " << e.what() << std::endl;
}

return 0;
}

输出:

姓名: 张三, 年龄: 30
姓名: 张三, 年龄: 30

在容器中使用 std::any

std::any 可以与标准容器一起使用,创建可以存储不同类型元素的异构容器:

cpp
#include <any>
#include <iostream>
#include <string>
#include <vector>

int main() {
std::vector<std::any> values;

values.push_back(42);
values.push_back(3.14);
values.push_back(std::string("Hello"));
values.push_back(true);

for (const auto& value : values) {
if (value.type() == typeid(int)) {
std::cout << "整数: " << std::any_cast<int>(value) << std::endl;
}
else if (value.type() == typeid(double)) {
std::cout << "浮点数: " << std::any_cast<double>(value) << std::endl;
}
else if (value.type() == typeid(std::string)) {
std::cout << "字符串: " << std::any_cast<std::string>(value) << std::endl;
}
else if (value.type() == typeid(bool)) {
std::cout << "布尔值: " << (std::any_cast<bool>(value) ? "true" : "false") << std::endl;
}
}

return 0;
}

输出:

整数: 42
浮点数: 3.14
字符串: Hello
布尔值: true

重置 std::any

可以使用 reset() 方法清除 std::any 对象中存储的值:

cpp
#include <any>
#include <iostream>

int main() {
std::any a = 42;
std::cout << "a 是否有值? " << (a.has_value() ? "是" : "否") << std::endl;

a.reset();
std::cout << "调用 reset() 后,a 是否有值? " << (a.has_value() ? "是" : "否") << std::endl;

return 0;
}

输出:

a 是否有值? 是
调用 reset() 后,a 是否有值? 否

实际应用场景

场景 1:插件系统

在插件系统中,std::any 可以用于传递不同类型的数据:

cpp
#include <any>
#include <iostream>
#include <string>
#include <unordered_map>

class PluginManager {
public:
void setParameter(const std::string& name, std::any value) {
parameters_[name] = std::move(value);
}

template<typename T>
T getParameter(const std::string& name) {
auto it = parameters_.find(name);
if (it != parameters_.end()) {
try {
return std::any_cast<T>(it->second);
}
catch (const std::bad_any_cast&) {
throw std::runtime_error("参数类型不匹配");
}
}
throw std::runtime_error("参数不存在");
}

private:
std::unordered_map<std::string, std::any> parameters_;
};

int main() {
PluginManager pm;

pm.setParameter("timeout", 30);
pm.setParameter("server", std::string("localhost"));
pm.setParameter("debug", true);

try {
int timeout = pm.getParameter<int>("timeout");
std::string server = pm.getParameter<std::string>("server");
bool debug = pm.getParameter<bool>("debug");

std::cout << "超时设置: " << timeout << " 秒\n";
std::cout << "服务器地址: " << server << "\n";
std::cout << "调试模式: " << (debug ? "开启" : "关闭") << std::endl;
}
catch (const std::exception& e) {
std::cout << "错误: " << e.what() << std::endl;
}

return 0;
}

输出:

超时设置: 30 秒
服务器地址: localhost
调试模式: 开启

场景 2:消息队列或事件系统

std::any 可以用于实现消息队列或事件系统,其中消息或事件可以携带不同类型的数据:

cpp
#include <any>
#include <iostream>
#include <queue>
#include <string>

struct Event {
std::string type;
std::any data;
};

class EventSystem {
public:
void pushEvent(const std::string& type, const std::any& data) {
events_.push(Event{type, data});
}

bool pollEvent(Event& event) {
if (events_.empty()) {
return false;
}
event = events_.front();
events_.pop();
return true;
}

private:
std::queue<Event> events_;
};

int main() {
EventSystem es;

// 发布事件
es.pushEvent("click", std::pair<int, int>(100, 200)); // 鼠标点击坐标
es.pushEvent("key_press", 'A'); // 按键事件
es.pushEvent("window_resize", std::pair<int, int>(800, 600)); // 窗口大小改变

// 处理事件
Event event;
while (es.pollEvent(event)) {
std::cout << "事件类型: " << event.type << std::endl;

if (event.type == "click") {
auto pos = std::any_cast<std::pair<int, int>>(event.data);
std::cout << "鼠标点击坐标: (" << pos.first << ", " << pos.second << ")" << std::endl;
}
else if (event.type == "key_press") {
char key = std::any_cast<char>(event.data);
std::cout << "按键: " << key << std::endl;
}
else if (event.type == "window_resize") {
auto size = std::any_cast<std::pair<int, int>>(event.data);
std::cout << "新窗口尺寸: " << size.first << "x" << size.second << std::endl;
}
std::cout << std::endl;
}

return 0;
}

输出:

事件类型: click
鼠标点击坐标: (100, 200)

事件类型: key_press
按键: A

事件类型: window_resize
新窗口尺寸: 800x600

std::any vs 其他类型擦除方案

与 void* 比较

  • std::any 提供类型安全,而 void* 不提供
  • std::any 管理对象的生命周期,而使用 void* 需要手动管理内存
  • std::any 支持任何可复制构造的类型,包括值类型和引用类型

与 std::variant 比较

  • std::any 可以存储任意类型,而 std::variant 只能存储预定义的类型
  • std::variant 通常更高效,因为它不使用动态内存分配
  • 当你知道所有可能的类型时,应该使用 std::variant;当类型不确定时,使用 std::any

与继承和多态比较

  • std::any 不需要预先定义类层次结构
  • 继承和多态通常更适合面向对象的设计,而 std::any 更适合通用容器和接口

性能考虑

使用 std::any 会带来一些性能开销:

  1. 动态内存分配:std::any 通常会在堆上分配内存
  2. 类型擦除和恢复:需要额外的运行时检查
  3. 移动/复制开销:存储和获取值时会涉及复制操作
警告

当性能是关键因素时,应该避免在热路径(频繁执行的代码路径)上过度使用 std::any

总结

std::any 是 C++17 引入的一个强大功能,它提供了一种类型安全的方式来存储任意类型的值。它主要具有以下特点:

  1. 可以存储任何可复制构造的类型
  2. 提供类型安全的值访问方式(通过 std::any_cast
  3. 自动管理所存储对象的生命周期
  4. 适用于需要存储未知类型或多种不同类型的场景

虽然 std::any 很强大,但它并不适合所有场景。当你知道可能的类型集合时,std::variant 可能是更好的选择;当性能至关重要时,可能需要使用更轻量级的解决方案。

练习

  1. 编写一个简单的命令行计算器,使用 std::any 存储计算结果,支持整数和浮点数计算。
  2. 创建一个简单的配置系统,使用 std::unordered_map<std::string, std::any> 来存储不同类型的配置值。
  3. 实现一个简单的 JSON 解析器,使用 std::any 表示 JSON 值(可以是数字、字符串、数组、对象等)。

额外资源

通过学习 std::any,你已经掌握了 C++17 中一个非常有用的特性,它能够帮助你编写更灵活、更通用的代码,同时保持类型安全。