# 存储不同的类型——std::variant

C++中支持使用`struct`和`class`的方式将不同类型的变量进行包装。当我们想要使用一种类型来表示多种类型时，也可以使用`union`。不过`union`的问题在于我们无法知道，其是以哪种类型为基础进行的初始化。

看一下下面的代码：

```cpp
union U {
    int a;
    char *b;
    float c;
};
void func(U u) { std::cout << u.b << '\n'; }
```

当我们调用`func`时，其会将已整型`a`为基础进行初始化的联合体`t`进行打印，当然也无法阻止我们对其他成员进行访问，就像使用字符串指针对成员`b`进行初始化了一样，这段代码会引发各种bug。当我们开始对联合体进行打包之前，有一种辅助变量能够告诉我们其对联合体进行的初始化是安全的，其就是`std::variant`，在C++17中加入STL。

`variant`是一种新的类型，类型安全，并高效的联合体类型。其不使用堆上的内存，所以在时间和空间上都非常高效。基于联合体的解决方案，我们就不用自己再去进行实现了。其能单独存储引用、数组或`void`类型的成员变量。

本节中，我们将会了解一下由`vriant`带来的好处。

## How to do it...

我们实现一个程序，其中有两个类型：`cat`和`dog`。然后将猫狗混合的存储于一个列表中，这个列表并不具备任何运行时多态性：

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

   ```cpp
   #include <iostream>
   #include <variant>
   #include <list>
   #include <string>
   #include <algorithm>

   using namespace std;
   ```
2. 接下来，我们将实现两个具有类似功能的类，不过两个类型之间并没有什么联系。第一个类型是`cat`。`cat`对象具有名字，并能喵喵叫：

   ```cpp
   class cat {
       string name;

   public:
       cat(string n) : name{n} {}

       void meow() const {
           cout << name << " says Meow!\n";
       }
   };
   ```
3. 另一个类是`dog`。`dog`能汪汪叫：

   ```cpp
   class dog {
       string name;

   public:
       dog(string n) : name{n} {}

       void woof() const {
           cout << name << " says Woof!\n";
       }
   };
   ```
4. 现在我们就可以来定义一个`animal`类型，其为`std::variant<dog, cat>`的别名类型。其和以前的联合体一样，同时具有`variant`的特性：

   ```cpp
   using animal = variant<dog, cat>;
   ```
5. 编写主函数之前，我们再来实现两个辅助者。其中之一为动物判断谓词，通过调用`is_type<cat>(...)`或`is_type<dog>(...)`，可以判断动物实例中的动物为`cat`或`dog`。其实现只需要对`holds_alternative`进行调用即可，其为`variant`类型的一个通用谓词函数：

   ```cpp
   template <typename T>
   bool is_type(const animal &a) {
       return holds_alternative<T>(a);
   }
   ```
6. 第二个辅助者为一个结构体，其看起来像是一个函数对象。其实际是一个双重函数对象，因为其`operator()`实现了两次。一种实现是接受`dog`作为参数输入，另一个实现是接受`cat`类型作为参数输入。对于两种实现，其会调用`woof`或`meow`函数：

   ```cpp
   struct animal_voice
   {
       void operator()(const dog &d) const { d.woof(); }
       void operator()(const cat &c) const { c.meow(); }
   };
   ```
7. 现在让我们使用这些辅助者和类型。首先，定义一个`animal`变量的实例，然后对其进行填充：

   ```cpp
   int main()
   {
       list<animal> l {cat{"Tuba"}, dog{"Balou"}, cat{"Bobby"}};
   ```
8. 现在，我们会将列表的中内容打印三次，并且每次都使用不同的方式。第一种使用`variant::index()`。因为`animal`类型是`variant<dog, cat>`类型的别名，其返回值的0号索引代表了一个`dog`的实例。1号索引则代表了`cat`的实例。这里的关键是变量特化的顺序。`switch-cast`代码块中，可以通过`get<T>`的方式获取内部的`cat`或`dog`实例：

   ```cpp
       for (const animal &a : l) {
           switch (a.index()) {
           case 0:
               get<dog>(a).woof();
               break;
           case 1:
               get<cat>(a).meow();
               break;
           }
       }
       cout << "-----\n";
   ```
9. 我们也可以显示的使用类型作为其索引。`get_if<dog>`会返回一个指向`dog`类型的指针。如果没有`dog`实例在列表中，那么指针则为`null`。这样，我们可以尝试获取下一种不同类型的实例，直到成功为止：

   ```cpp
       for (const animal &a : l) {
           if (const auto d (get_if<dog>(&a)); d) {
               d->woof();
           } else if (const auto c (get_if<cat>(&a)); c) {
               c->meow();
           }
       }
       cout << "-----\n";
   ```
10. 使用`variant::visit`是一种非常优雅的方式。这个函数能够接受一个函数对象和一个`variant`实例。函数对象需要对`variant`中所有可能类型进行重载。我们在之前已经对`operator()`进行了重载，所以这里可以直接对其进行使用：

    ```cpp
        for (const animal &a : l) {
            visit(animal_voice{}, a);
        }
        cout << "-----\n";
    ```
11. 最后，我们将回来数一下`cat`和`dog`在列表中的数量。`is_type<T>`的`cat`和`dog`特化函数，将会与`std::count_if`结合起来使用，用来返回列表中不同实例的个数：

    ```cpp
        cout << "There are "
            << count_if(begin(l), end(l), is_type<cat>)
            << " cats and "
            << count_if(begin(l), end(l), is_type<dog>)
            << " dogs in the list.\n";
    }
    ```
12. 编译并运行程序，我们就会看到打印三次的结果都是相同的。然后，可以看到`is_type`和`count_if`配合的很不错：

    ```cpp
    $ ./variant
    Tuba says Meow!
    Balou says Woof!
    Bobby says Meow!
    -----
    Tuba says Meow!
    Balou says Woof!
    Bobby says Meow!
    -----
    Tuba says Meow!
    Balou says Woof!
    Bobby says Meow!
    -----
    There are 2 cats and 1 dogs in the list.
    ```

## How it works...

`std::variant`与`std::any`类型很相似，因为这两个类型都能持有不同类型的变量，并且我们需要在运行时对不同对象进行区分。

另外，`std::variant`有一个模板列表，需要传入可能在列表中的类型，这点与`std::any`截然不同。也就是说`std::variant<A, B, C>`必须是A、B或C其中一种实例。当然这也意味着其就不能持有其他类型的变量，除了列表中的类型`std::variant`没有其他选择。

`variant<A, B, C>`的类型定义，与以下联合体定义类似：

```cpp
union U {
    A a;
    B b;
    C c;
};
```

当我们对`a`, `b`或`c`成员变量进行初始化时，联合体中对其进行构建机制需要我们自行区分。`std::variant`类型就没有这个问题。

本节的代码中，我们使用了三种方式来处理`variant`中成员的内容。

首先，使用了`variant`的`index()`成员函数。对变量类型进行索引，`variant<A, B, C>` 中，索引值0代表A类型，1为B类型，2为C类型，以此类推来访问复杂的`variant`对象。

下一种就是使用`get_if<T>`函数进行获取。其能接受一个`variant`对象的地址，并且返回一个类型`T`的指针，指向其内容。如果`T`类型是错误，那么返回的指针就为`null`指针。其也可能对`variant`变量使用`get<T>(x)`来获取对其内容的引用，不过当这样做失败时，函数将会抛出一个异常(使用get-系列函数进行转换之前，需要使用`holds_alternative<T>(x)`对其类型进行检查)。

最后一种方式就是使用`std::visit`函数来进行，其能接受一个函数对象和一个`variant`实例。`visit`函数会对`variant`中内容的类型进行检查，然后调用对应的函数对象的重载`operator()`操作符。

为了这个目的，我们实现为了`animal_voice`类型，将`visit`和`variant<dog, cat>`类型结合在了一起：

```cpp
struct animal_voice
{
    void operator()(const dog &d) const { d.woof(); }
    void operator()(const cat &c) const { c.meow(); }
};
```

以`visit`的方式对`variant`进行访问看起来更加的优雅一些，因为使用这种方法就不需要使用硬编码的方式对`variant`内容中的类型进行判别。这就让我们的代码更加容易扩展。

> Note：
>
> `variant`类型不能为空的说法并不完全正确。将[std::monostate](http://zh.cppreference.com/w/cpp/utility/variant/monostate)类型添加到其类型列表中，其就能持有空值了。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chenxiaowei.gitbook.io/c-17-stl-cook-book/chapter8-0-chinese/chapter8-7-chinese.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
