不直入主题

本篇之中,仅仅述及 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::variantstd::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::variantstd::optionalstd::any

它们的特点或者说区别也是很明显的:

  • variant 需要声明一组可用的数据类型,你可以在这组类型的范围之中来使用变体类型
  • any 允许你自由地使用变体类型
  • variant 和 any 都是变体类型,在变量的生命周期中,你可以为其赋以不同类型的值
  • optional 是一个现代 C++ 版的 Nullable<T> 工具。所以它是确定类型的。

然而,它们都不能确切地满足 cmdr 的需求。

:end: