一般来说,C++是一门类型绑定和类型安全的语言。 值对象被声明为确定的类型,这个类型定义了所有可能的操作、也定义了对象的行为。 而且,对象不能改变自身的类型。
std::any
是一种在保证类型安全的基础上还能改变自身类型的值类型。
也就是说,它可以持有任意类型的值,并且它知道自己当前持有的值是什么类型的。
当声明一个这种类型的对象时不需要指明所有可能的类型。
实现的关键在于std::any
对象同时包含了值和值的类型。
因为内含的值可以有任意的大小,所以可能会在堆上分配内存。
然而,实现应该尽量避免为小类型的值例如int
在堆上分配内存。
对于一个std::any
对象,如果你赋值为一个字符串,它将会分配内存并拷贝字符串,
并存储记录当前的值是一个字符串。之后,可以使用运行时检查来判断当前的值的类型。
为了将当前的值转换为真实的类型,必须要使用any_cast<>
。
和std::optional<>、std::variant<>
一样,std::any
对象有值语义。
也就是说,拷贝被实现为 深拷贝 ,会创建一个在自己的内存中持有当前值的独立对象。
因为可能会使用堆内存,所以拷贝std::any
的开销一般都很大。
更推荐以引用传递对象,或者move值。std::any
支持部分move语义。
下面的示例演示了std::any
的核心能力:
std::any a; // a为空
std::any b = 4.3; // b有类型为double的值4.3
a = 42; // a有类型为int的值42
b = std::string{"hi"}; // b有类型为std::string的值"hi"
if (a.type() == typeid(std::string)) {
std::string s = std::any_cast<std::string>(a);
useString(s);
}
else if (a.type() == typeid(int)) {
useInt(std::any_cast<int>(a));
}
你可以声明一个空的std::any
,也可以用某个类型的值初始化。
如果传递了初始值,std::any
内含的值的类型将变为初始值的类型。
通过使用成员函数type()
,你可以检查内含值的类型和某一个类型的ID是否相同。
如果对象是空的,类型ID将等于typeid(void)
。
为了访问内部的值,你必须使用std::any_cast<>
将它转换为真正的类型:
auto s = std::any_cast<std::string>(a);
如果转换失败,可能是因为对象为空或者与内部值的类型不匹配,这时会抛出一个
std::bad_any_cast
异常。因此,在不知道当前类型的情况下,
你最好像下面这样使用:
try {
auto s = std::any_cast<std::string>(a);
...
}
catch (std::bad_any_cast& e) {
std::cerr << "EXCEPTION: " << e.what() << '\n';
}
注意std::any_cast<>
会创建一个指定类型的对象。
例如这个例子中,如果你用std::string
作为std::any_cast<>
的模板参数,
它将创建一个临时的字符串(prvalue),然后使用临时字符串初始化新的对象s
。
如果不需要初始化其他变量,更推荐转换为引用类型来避免创建临时对象:
std::cout << std::any_cast<const std::string&>(a);
如果想要修改当前的值,也需要转换为相应的引用类型:
std::any_cast<std::string&>(a) = "world";
你也可以对一个std::any
对象的地址调用std::any_cast
。
这种情况下,如果类型匹配那么转换结果将会是一个相应类型的指针,否则将返回nullptr
:
auto p = std::any_cast<std::string>(&a);
if (p) {
...
}
或者,可以使用新的带初始化的if
语句:
if (auto p = std::any_cast<std::string>(&a); p != nullptr) {
...
}
或者:
if (auto p = std::any_cast<std::string>(&a)) {
...
}
为了清空一个std::any
对象,你可以调用:
a.reset(); // 清空对象
或者:
a = std::any{};
或者:
a = {};
你也可以直接检查对象是否有值:
if (a.has_value()) {
...
}
注意存储值时类型会退化(数组转换为指针,顶层引用和const
被忽略)。
对于字符串字面量,值类型将是const char*
。
为了使用std::any_cast<>
进行转换,你必须显式指明这个类型:
std::any a = "hello"; // type()是const char*
if (a.type() == typeid(const char*)) { // true
...
}
if (a.type() == typeid(std::string)) { // false
...
}
std::cout << std::any_cast<const char*>(a) << '\n'; // OK
std::cout << std::any_cast<std::string>(a) << '\n'; // EXCEPTION
这基本就是std::any
支持的所有操作了。
没有定义比较运算符(这意味着你不能比较或者排序对象)。
没有定义哈希函数,也没有定义value()
成员函数。
而且,因为类型只有在运行时才能获取,所以也不能使用泛型lambda来独立于类型处理当前的值。
你只能使用运行时的std::any_cast<>
函数来处理当前的值,
这意味着处理当前值时你需要一些类型特定的代码来重入C++的类型系统。
然而,你可以把std::any
对象放置在容器中。例如:
std::vector<std::any> v;
v.push_back(42);
std::string s = "hello";
v.push_back(s);
for (const auto& a : v) {
if (auto pa = std::any_cast<const std::string>(&a); pa != nullptr) {
std::cout << "string: " << *pa << '\n';
}
else if (auto pa = std::any_cast<const int>(&a); pa != nullptr) {
std::cout << "int: " << *pa << '\n';
}
}
注意你应该总是使用这样的if-else链。这里不能使用switch
语句。
这一节详细描述std::any
的类型和操作。
在头文件中,C++标准库以如下方式定义了类std::any
:
namespace std {
class any;
}
也就是说,std::any
根本就不是模板类。
另外,还定义了下面的类型和对象:
- 异常类
std::bad_any_cast
,当转换失败时会抛出这种异常。 这个类派生自std::bad_cast
,后者又派生自std::exception
。
std::any
类也使用了定义在头文件<utility>
中的
std::in_place_type
对象(类型是std::in_place _type_t
)。
表std::any
的操作列出了std::any
的所有操作:
操作 | 效果 |
---|---|
构造函数 | 创建一个any对象(可能会调用底层类型的构造函数) |
make_any<>() |
创建一个any对象(传递参数来初始化) |
析构函数 | 销毁any对象 |
= |
赋予新值 |
emplace<T>() |
赋予一个类型T 的新值 |
reset() |
销毁any类型(使对象变为空) |
has_value() |
返回对象是否持有值 |
type() |
以std::type_info 对象返回当前类型 |
any_cast<T>() |
将当前值转换为类型T 的值(如果类型不正确将抛出异常/返回nullptr ) |
swap() |
交换两个对象的值 |
默认情况下,std::any
被初始化为空。
std::any a1; // a1是空的
如果传递值来初始化,内含值的类型将是它 退化 后的类型:
std::any a2 = 42; // a2包含int类型的值
std::any a3 = "hello"; // a2包含const char*类型的值
为了使内部值的类型和初始值的类型不同,你需要使用in_place_type
标记:
std::any a4{std::in_place_type<long>, 42};
std::any a5{std::in_place_type<std::string>, "hello"};
传给in_place_type
的类型也可能退化。
下面的代码声明了一个持有const char*
的对象:
std::any a5b{std::in_place_type<const char[6]>, "hello"};
为了用多个参数初始化std::any
对象,你必须手动创建对象或者你可以添加
std::in_place_type
作为第一个参数(因为内含类型不能直接从多个初始值推导出来):
std::any a6{std::complex{3.0, 4.0}};
std::any a7{std::in_place_type<std::complex<double>>, 3.0, 4.0};
你甚至可以传递初值列和其他参数:
// 用一个以lambda为排序准则的set初始化std::any对象
auto sc = [] (int x, int y) {
return std::abs(x) < std::abs(y);
};
std::any a8{std::in_place_type<std::set<int, decltype(sc)>>, {4, 8, -7, -2, 0, 5}, sc};
注意还有一个快捷函数make_any<>()
,它可以接受一个或多个参数
(不需要使用in_place_type
参数)。
你必须显式指明初始化的类型(即使只有一个参数它也不会自动推导类型):
auto a10 = std::make_any<float>(3.0);
auto a11 = std::make_any<std::string>("hello");
auto a13 = std::make_any<std::complex<double>>(3.0, 4.0);
auto a14 = std::make_any<std::set<int, decltype(sc)>> ({4, 8, -7, -2, 0, 5}, sc);
可以使用赋值操作和emplace()
来修改值。例如:
std::any a;
a = 42; // a含有int类型的值
a = "hello"; // a含有const char*类型的值
a.emplace<std::string>("hello"); // a含有std::string类型的值
a.emplace<std::complex<double>>(4.4, 5.5); // a含有std::complex<double>类型的值
为了访问内含的值,你必须使用std::any_cast<>
将它转换为真实的类型。
例如为了把值转换成string,你有以下几种方法:
std::any_cast<std::string>(a); // 返回值的拷贝
std::any_cast<std::string&>(a); // 通过引用获取写权限
std::any_cast<const std::string&>(a); // 通过引用获取读权限
如果移除掉顶层引用和const
之后类型ID相同则说明类型匹配。
如果转换失败,将会抛出一个std::bad_any_cast
异常。
为了避免异常处理,你可以传递any
对象的地址。
当因为类型不匹配导致转换失败时它会返回nullptr
:
if (auto sp{std::any_cast<std::string>(&a)}; sp != nullptr) {
... // 使用*sp获取a的值的写权限
}
if (auto sp{std::any_cast<const std::string>(&a)}; sp != nullptr) {
... // 使用*sp获取a的值的读权限
}
注意,这里如果转换为引用将导致运行时错误:
std::any_cast<std::string&>(&a); // 运行时错误
std::any
也支持move语义。然而,注意前提是底层类型要支持拷贝语义。
也就是说 move-only类型 不支持作为内含的值类型。
处理move语义的最佳方式可能不是很明显,你可以这样做:
std::string s("hello, world!");
std::any a;
a = std::move(s); // 把s移进a
s = std::move(std::any_cast<std::string&>(a)); // 把a中的string移动到s
注意像通常一样,值被移走的对象处于仍然有效但是值未定义的状态。
因此,你可以继续将a
用作字符串,只要不对它的值做任何假设。
下面的语句将 不会 输出"NIL"
,值被移走的字符串一般是空字符串
(但也可能有其他的值):
std::cout << (a.has_value() ? std::any_cast<std::string>(a) : std::string("NIL"));
注意:
s = std::any_cast<std::string>(std::move(a));
也能生效,但需要一次额外的move。
直接转换成右值引用将不能通过编译:
s = std::any_cast<std::string&&>(a); // 编译期error
注意和如下调用
a = std::move(s); // 把s移进a
相比,下面的代码 有可能不能 工作(即使这是C++标准里的一个例子):
std::any_cast<string&>(a) = std::move(s); // OOPS:a必须持有string
只有当a
已经包含有一个std::string
类型的值时这段代码才能工作。
否则,在我们赋予新值之前,这个转换就会抛出std::bad_any_cast
异常。