不直入主题
本篇之中,仅仅述及 std::any
,也暂时是这批和 variant 相关的话题的最后一篇了。
但开篇之前,要分享一个经验:
如果使用 ruby 环境例如挂着 jekyll 服务器在本地写 gh-pages,然后去 Finder 中复制一篇旧 Post 副本,点它,你想的是要改名做一篇新 Post 对吗,但 big sur 会死,死了之后会重启。
我 #%^@*#。
所以首先我花费一小节去研究 ruby 要不要升级,吗?
终端窗口中看得到,当我 duplicate 一篇文章时,Jekyll 正在卖力地 generating htmls,此时 rename in finder 总是死,这几天凌晨写 posts,今天遇到第二次,所以确定这个路数,我吃了这包药了。可惜的是,这特么连搜索解决方案都不可能,根本无法构造一个问问题的句子。
它看起来不像是真的 ruby 或者 Jekyll 的问题,然而我这里几乎没有 fuse 类的驱动了,NTFS driver 也都清理得干干净净了,只剩下 iCloud 了。
C++17 之前
std::any
有点像 Nullable Variant,在早前似乎没有严格的对应物。不过,早期的多种 variant 实现都支持 NULL 指针对象的放入,所以 std::any
也可以与它们勉强适配以资对照。
std::any in C++17
由于早前两篇文章介绍 std::variant
和 std::optional
时已经提前做了不少的比较,所以本篇之中再来提及 std::any
时,也没有太多稀奇的东西了。
首先你已经知道,std::any
是一个确切的变体类型,这意味着在其变量生存周期中,你可以随时放入不同数据类型的值,可以先放入 string,然后在放入 float,没有问题。
而且在这一点上,它比 std::variant
更加放荡。std::variant
说,咱们提前说好了,只有黄金裘的人儿可以进来,然后你就只能放入黄金裘的值。但 std::any
说的是,我不管,我不管,谁都可以进来。所以无需特别声明一个允许的数据类型列表,你可以直接使用 std::any
,而且放入的数据类型没有限制。
当然,尽管这么说,你还是不应放入引用类型。
使用
用法下面做一些小小的示例。
包含头文件
1
#include <any>
声明变量
1
2
3
4
5
6
std::any a = 1;
std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
a = 3.14;
std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';
a = true;
std::cout << a.type().name() << ": " << std::any_cast<bool>(a) << '\n';
转换类型
1
2
3
4
5
6
7
8
9
10
// 有误的转型
try
{
a = 1;
std::cout << std::any_cast<float>(a) << '\n';
}
catch (const std::bad_any_cast& e)
{
std::cout << e.what() << '\n';
}
测试有无有效值
1
2
3
4
5
6
7
8
9
10
11
12
a = 1;
if (a.has_value())
{
std::cout << a.type().name() << '\n';
}
// 重置
a.reset();
if (!a.has_value())
{
std::cout << "no value\n";
}
可以对 std::any
拥有的值做取地址的操作,该指针的可靠的:
1
2
3
4
// 指向所含数据的指针
a = 1;
int* i = std::any_cast<int>(&a);
std::cout << *i << "\n";
增强的构造
std::any
同样也有原位构造以及赋值 emplace,支持 swap,也支持 any 之间的复制。
使用 make_any
1
auto a0 = std::make_any<std::string>("Hello, std::any!\n");
typeid 和观察器
std::any
有一个特别的 type() 函数能够返回所包含值的 typeid。
所以可以有几种方式来尝试从 any 中抽出值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void ingest_any(const std::any &any) {
try {
std::cout << std::any_cast<std::string>(any) << "\n";
} catch (std::bad_any_cast const &) {}
if (std::string *str = std::any_cast<std::string>(&any)) {
std::cout << *str << "\n";
}
if (std::type_index{typeid(std::string)} == any.type()) {
// Known not to throw, as previously checked.
std::cout << std::any_cast<std::string>(any) << "\n";
}
}
除此而外,visitor 模式也可以,但需要稍稍有所动作,因为你需要自行编写 visitor 工具:
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
#include <any>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
template<class Visitor>
void visit_any_as(std::any const &, Visitor &&) {
throw std::logic_error("std::any contained no suitable type, unable to visit");
}
template<class First, class... Rest, class Visitor>
void visit_any_as(std::any const &any, Visitor &&visitor) {
First const *value = std::any_cast<First>(&any);
if (value) {
visitor(*value);
} else {
visit_any_as<Rest...>(any, std::forward<Visitor>(visitor));
}
}
int main(){
std::any any{-1LL};
try {
visit_any_as<std::string, int, double, char>(any, [](auto const &x) {
std::cout << x << std::endl;
});
}
catch (std::exception const &e) {
std::cout << e.what() << std::endl;
}
}
这是来自于 Roman Odaisky 的网络回答 中的样例,非常精炼,所以直接取用了(略有调整以便能跑)。
但这个 visitor 要求你必须提供已知的类型的具体版本,所以使用它需要研究具体场合。有的时候可能还是直接 any_cast 来得简便,只不过代码看起来有点恶形而已——特别是连续处理几种数据类型时。
小小结
std::any
是一个有力的类模板,然而你要想借助它来操作变体类型的话,手脚还是有点麻烦的,主要问题在于取用时需要一些冗长的编码才行。
小结
这一次,最近几篇文章中,我们已经规规矩矩地回顾了 C++17 中的新模板工具类:std::variant
,std::optional
,std::any
。
它们的特点或者说区别也是很明显的:
- variant 需要声明一组可用的数据类型,你可以在这组类型的范围之中来使用变体类型
- any 允许你自由地使用变体类型
- variant 和 any 都是变体类型,在变量的生命周期中,你可以为其赋以不同类型的值
- optional 是一个现代 C++ 版的
Nullable<T>
工具。所以它是确定类型的。
然而,它们都不能确切地满足 cmdr 的需求。
留下评论