void*替换为更为安全的std::any

有时我们会需要将一个变量保存在一个未知类型中。对于这样的变量,我们通常会对其进行检查,以确保其是否包含一些信息,如果是包括,那我们将会去判别所包含的内容。以上的所有操作,都需要在一个类型安全的方法中进行。

以前,我们会将可变对象存与void*指针当中。void类型的指针无法告诉我们其所指向的对象类型,所以我们需要将其进行手动转换成我们期望的类型。这样的代码看起来很诡异,并且不安全。

C++17在STL中添加了一个新的类型——std::any。其设计就是用来持有任意类型的变量,并且能提供类型的安全检查和安全访问。

本节中,我们将会来感受一下这种工具类型。

How to do it…

我们将实现一个函数,这个函数能够打印所有东西。其就使用std::any作为参数:

  1. 包含必要的头文件,并声明所使用的命名空间:

    1. #include <iostream>
    2. #include <iomanip>
    3. #include <list>
    4. #include <any>
    5. #include <iterator>
    6. using namespace std;
  2. 为了减少后续代码中尖括号中的类型数量,我们对list<int>进行了别名处理:

    1. using int_list = list<int>;
  3. 让我们实现一个可以打印任何东西的函数。其确定能打印任意类型,并以std::any作为其参数:

    1. void print_anything(const std::any &a)
    2. {
  4. 首先,要做的事就是对传入的参数进行检查,确定参数中是否包含任何东西,还是只是一个空实例。如果为空,那就没有必要再进行接下来的打印了:

    1. if (!a.has_value()) {
    2. cout << "Nothing.\n";
  5. 当非空时,就要需要对其进行类型比较,直至匹配到对应类型。这里第一个类型为string,当传入的参数是一个string,我们可以使用std::any_casta转化成一个string类型的引用,然后对其进行打印。我们将双引号当做打印字符串的修饰:

    1. } else if (a.type() == typeid(string)) {
    2. cout << "It's a string: "
    3. << quoted(any_cast<const string&>(a)) << '\n';
  6. 当其不是string类型时,其也可能是一个int类型。当与之匹配是使用any_cast<int>a转换成int型数值:

    1. } else if (a.type() == typeid(int)) {
    2. cout << "It's an integer: "
    3. << any_cast<int>(a) << '\n';
  7. std::any并不只对stringint有效。我们将maplist,或是更加复杂的数据结构放入一个any变量中。让我们输入一个整数列表看看,按照我们的预期,函数也将会打印出相应的列表:

    1. } else if (a.type() == typeid(int_list)) {
    2. const auto &l (any_cast<const int_list&>(a));
    3. cout << "It's a list: ";
    4. copy(begin(l), end(l),
    5. ostream_iterator<int>{cout, ", "});
    6. cout << '\n';
  8. 如果没有类型能与之匹配,那就不会进行猜测了。我们会放弃对类型进行匹配,然后告诉使用者,我们对输入毫无办法:

    1. } else {
    2. cout << "Can't handle this item.\n";
    3. }
    4. }
  9. 主函数中,我们能够对调用函数传入任何类型的值。我们可以使用大括号对来构建一个空的any变量,或是直接输入字符串“abc”,或是一个整数。因为std::any可以由任何类型隐式转换而成,这里并没有语法上的开销。我们也可以直接构造一个列表,然后丢入函数中:

    1. int main()
    2. {
    3. print_anything({});
    4. print_anything("abc"s);
    5. print_anything(123);
    6. print_anything(int_list{1, 2, 3});
  10. 当我们想要传入的参数比较大,那么拷贝到any变量中就会花费很长的时间,这是可以使用立即构造的方式。in_place_type_t<int_list>{}表示一个空的对象,对于any来说其就能够知道应该如何去构建对象了。第二个参数为{1,2,3}其为一个初始化列表,其会用来初始化int_list对象,然后被转换成any变量。这样,我们就避免了不必要的拷贝和移动:

    1. print_anything(any(in_place_type_t<int_list>{}, {1, 2, 3}));
    2. }
  11. 编译并运行程序,我们将得到如下的输入出:

    1. $ ./any
    2. Nothing.
    3. It's a string: "abc"
    4. It's an integer: 123
    5. It's a list: 1, 2, 3,
    6. It's a list: 1, 2, 3,

How it works…

std::any类型与std::optional类型很类似——具有一个has_value()成员函数,能告诉我们其是否携带一个值。不过这里,我们还需要对字面的数据进行保存,所以any要比optional类型复杂的多。

访问any变量的内容前,我们需要知道其所承载的类型,然后将any变量转换成那种类型。

这里,使用的比较方式为x.type == typeid(T)。如果比较结果匹配,那么就使用any_cast对其内容进行转换。

需要注意的是any_cast<T>(x)将会返回xT值的副本。如果想要避免对复杂对象不必要的拷贝,那就需要使用any_cast<T&>(x)。本节的代码中,我们使用引用的方式来获取stringlist<int>对象的值。

Note:

如果any变量转换成为一种错误的类型,其将会抛出std::bad_any_cast异常。