C++ 可变体(variant)

一、可变体(variant) 基础用法

Union的问题:

  • 无法知道当前使用的类型是什么。
  • 而且union无法自动调用底层数据成员的析构函数。
  • 创建复杂的数据类型的封装能力非常鸡肋.

variant

C++17 提供了 std::variant

可变体的声明

下面的代码是声明一个可变体的用法,在variant关键字的尖括号内,依次指定可变体的的数据类型。在可变体的内部,这些数据类型存在顺序关系

int main()
{
    //声明一个可变体的对象
    std::variant<int, double, std::string> tmp;
}

可变体的辅助函数

C++17标准中还提供了一些常用可变体的辅助函数模板的API

  • std::variant_size_v——用于检测可变体内部可切换的数据类型的个数
int main()
{
    //声明一个可变体的对象
    std::variant<int, double, std::string> tmp;
    static_assert(std::variant_size_v<decltype(tmp)> == 3);   // static_assert静态断言,如果表达式为false会在编译时报错  
}
  • std::visit——用于访问可变体中的当前处于活动状态的数据类型的实例(即当前在使用的类型实例)
  • index方法返回当前可变体内部对应的数据类型的索引
#include

struct PrintVisitor  {  //visitor
    void operator()(int i) {std::cout << "int: " << i << '\n';}
    void operator()(double i) {std::cout << "double: " << i << '\n';}
    void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};

int main()
{
    std::variant<int, double, std::string> tmp;
    static_assert(std::variant_size_v<decltype(tmp)> == 3);

    // default initialized to the first alternative, should be 0
    std::visit(PrintVisitor {}, tmp);
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;

    tmp = 100.00;
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;
    std::visit(PrintVisitor {}, tmp);
    tmp = "hello super world";
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;
    std::visit(PrintVisitor {}, tmp);

}
  • 当对可变体赋值的数据类型是float,那么可变体对象tmp内部就会自动切换为float。
  • 当对可变体赋值的数据类型是string,那么可变体对象tmp内部就会自动切换为string。

C++ 可变体(variant)_第1张图片

std::visit简单来说就是;用来给可变体内的每一个数据类型添加上相应的动作,例如:

#include

struct PrintVisitor {  //visitor
    void operator()(int i) {std::cout << "int: " << i << '\n';}
    void operator()(double i) {std::cout << "double: " << i << '\n';}
    void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};

int main()
{
    std::variant<int, double, std::string> value = "123";
    static_assert(std::variant_size_v<decltype(value)> == 3, "error");
    std::visit(PrintVisitor{}, value);
    return 0;
}

在这里插入图片描述
还有一种更为高效的方式:

#include

int main()
{
    std::variant<int, double, std::string> value = 1.123;
    static_assert(std::variant_size_v<decltype(value)> == 3, "error");
    std::visit(
        [](auto &&arg) {
            //using C++17提供的重命名
            using T = std::decay_t<decltype(arg)>;  // 类型退化,去掉类型中的const 以及 &
            if constexpr(std::is_same_v<T, int>) {     //编译时if,只有被选中的if constexpr分支才会被实例化。
                std::cout << "int: " << arg << '\n';
            } else if constexpr(std::is_same_v<T, double>) {   //std::is_same_v:判断输入的类型是否是指定的模板类型
                std::cout<< "double: "<< arg <<'\n';
            } else if constexpr(std::is_same_v<T, std::string>) {
                std::cout<< "string: "<< arg <<'\n';
            }
        }
        , value);

    return 0;
}

在这里插入图片描述
这种方式高效的原因在于它是在编译期完成的类型判断。

std::visit的参数列表是不定长的,可以传入多个variant变量:

template <class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variant&&... vars);
  • std::get_if和std::get的区别
    两个方法的参数都可以是index(下标)或者T(类型)。
    当外部代码尝试获取可变体对应的数据类型的值,那么使用 std::get_if 或std::get 访问该数据类型的值(但这可能会引发bad_variant_access 异常)。通常get_if保证std::get在访问可变体时不会抛出bad_variant_access 异常,提供了访问前的类型安全判断

  • hold_alternative<> —— 判断可变体当前持有的数据类型

#include

struct PrintVisitor  {  //visitor
    void operator()(int i) {std::cout << "int: " << i << '\n';}
    void operator()(double i) {std::cout << "double: " << i << '\n';}
    void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};

int main()
{
    std::variant<int, double, std::string> tmp;
    static_assert(std::variant_size_v<decltype(tmp)> == 3);

    // default initialized to the first alternative, should be 0
    std::visit(PrintVisitor {}, tmp);
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;

    tmp = 100.0f;
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;
    std::visit(PrintVisitor {}, tmp);
    tmp = "hello super world";
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;
    std::visit(PrintVisitor {}, tmp);


    //当前tmp存的是string类型值
    if(const auto intPtr (std::get_if<int>(&tmp)); intPtr)    //intPtr不为真,所以不会执行
        std::cout << "int! " << *intPtr << '\n';
    if(const auto doublePtr (std::get_if<double>(&tmp)); doublePtr)   //doublePtr不为真,所以不会执行
        std::cout << "int! " << *doublePtr << '\n';

    if(std::holds_alternative<int>(tmp))
        std::cout << "可变体持有int类型\n";
    else if(std::holds_alternative<double>(tmp))
        std::cout << "可变体持有double类型\n";
    else if(std::holds_alternative<std::string>(tmp))
        std::cout << "可变体持有string类型\n";
}

C++ 可变体(variant)_第2张图片

  • 访问可变体的异常处理

为了给可变体的访问增强类型安全,在上下文可以增加bad_variant_access的异常检测。下面是一个异常处理的示例。由于当前的可变体对象内部活动类型是string。因此尝试get< double>(tmp)、get< 0 >(tmp)、get< 1 >(tmp)这类的访问操作都会抛出bad_variant_access异常。

#include

struct PrintVisitor  {  //visitor
    void operator()(int i) {std::cout << "int: " << i << '\n';}
    void operator()(double i) {std::cout << "double: " << i << '\n';}
    void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};

int main()
{
    std::variant<int, double, std::string> tmp;
    static_assert(std::variant_size_v<decltype(tmp)> == 3);

    // default initialized to the first alternative, should be 0
    std::visit(PrintVisitor {}, tmp);
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;

    tmp = 100.0f;
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;
    std::visit(PrintVisitor {}, tmp);
    tmp = "hello super world";
    std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;
    std::visit(PrintVisitor {}, tmp);


    //当前tmp存的是string类型值
    if(const auto intPtr (std::get_if<int>(&tmp)); intPtr)    //intPtr不为真,所以不会执行
        std::cout << "int! " << *intPtr << '\n';
    if(const auto doublePtr (std::get_if<double>(&tmp)); doublePtr)   //doublePtr不为真,所以不会执行
        std::cout << "int! " << *doublePtr << '\n';

    if(std::holds_alternative<int>(tmp))
        std::cout << "可变体持有int类型\n";
    else if(std::holds_alternative<double>(tmp))
        std::cout << "可变体持有double类型\n";
    else if(std::holds_alternative<std::string>(tmp))
        std::cout << "可变体持有string类型\n";

    try
    {
        /* code */
        auto f = std::get<double>(tmp);
        std::cout << "double! " << f << '\n';
    }
    catch(std::bad_variant_access&)
    {
        std::cout << "可变体内部当前持有的数据类型和get<>的传入参数类型不一致" << '\n';
    }
}

C++ 可变体(variant)_第3张图片

小结:

  • 可通过hold_alternative当前使用的类型。
  • 可变体不允许获取非活动类型的值。
  • 可变体不会发生额外的堆内存分配。
  • 可以使用std::visit对当前保留类型调用某些操作。
  • 没有通过赋值的初始化可变体,则可变体默认使用声明中的第一种类型来初始化可变体,在这种情况下,第一个声明的类型必须具有默认构造函数

二、可变体(variant)的初始化

variant的构造

针对聚合类型的variant构造

下面代码定义了ItCat这个类,并且在声明可变体的第一个类型参数就是ItCat,不用问这段代码报错的原因其实很简单,因为ItCat没有显式提供默认的构造器。
C++ 可变体(variant)_第4张图片
那么给他ItCat这个用户自定义类型加一个默认构造器,那么在可变体在初始化过程中,就能从类型参数列表中的第一个ItCat获得一个默认构造器。
C++ 可变体(variant)_第5张图片

可变体的类型模糊的传参构造

#include

class ItCat {
public:
    ItCat()=default;
    ItCat(int, float) {}
};

int main()
{
    std::variant<ItCat, int, float, double> tmp = 1.34;
    std::cout << tmp.index() << '\n';
}

在这里插入图片描述

对于可变体声明中的参数列表,int、float、double它们可相互转换的数据类型,但对于强调类型安全的C++编译器来说,无疑是给它增加困扰,而C++编译器对待这种模棱两可的值,它默认匹配值的数据类型是确保值的最大精度。因此C++编译器会让可变体选择中的double类型。

std::monostate

为了支持第一个类型没有默认构造函数的variant对象,提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态,因此,它们总是相等的。它自己的目的是表示另一种类型,这样variant就没有任何其他类型的值。也就是说,std::monostate可以作为第一种替代类型,使变体类型默认为可构造的。

std::variant<std::monostate, NoDefConstr> v2; // OK
std::cout << "index: " << v2.index() << '\n'; // prints 0

std::in_place_index函数接口

为了解决传值无棱两可的问题,C++17的的API库提供了std::in_place_index函数接口。下面是使用例子:

#include


class ItCat {
public:
    ItCat()=default;
    ItCat(int, float) {}
};

int main()
{
    std::variant<ItCat, int, float, double> tmp(std::in_place_index<2>, 1.34);
    std::cout << tmp.index() << '\n';
}

在这里插入图片描述

容器级别传值的variant构造

对于容器级传参的variant初始化问题,就必须显式调用std::in_place_index告知可变体对象要在内置启用哪一个数据类型来构造可变体对象的实例。如下:

#include


class ItCat {
public:
    ItCat()=default;
    ItCat(int, float) {}
};

int main()
{
    std::variant<ItCat, int, std::vector<int>, double> tmp(std::in_place_index<2>, {1, 2, 3, 4, 5});
    std::cout << std::get<std::vector<int>>(tmp).size() << '\n';
}

小结:

默认情况下,变体对象使用第一种类型进行初始化,如果类型没有默认构造函数的情况下,会得到一个编译器错误。在这种情况下,应使用 std::monostate 将其作为第一种类型传递。

三、可变体内对象成员的生命周期和访问者模式

修改可变体的对象成员

  • 方式1:赋值操作符
  • 方式2:通过get方法获取真正的对象,然后修改
  • 方式3:通过原地索引API匹配数据类型,然后构造传值达到修改值的目的。
#include


class ItCat {
public:
    ItCat()=default;
    ItCat(int, float) {}
};

int main()
{
    using Mixtype = std::variant<ItCat, int, std::vector<int>, std::string, double>;
    Mixtype tmp;


    //方式1:赋值操作符
    tmp=12;   //此时为int
    std::cout << tmp.index() << '\n';
    std::cout<< std::get<1>(tmp) << '\n';

    tmp = 23.5; //此时为double
    std::cout<< std::get<4>(tmp) << '\n';


    //方式2:通过get方法获取真正的对象,然后修改
    std::get<4>(tmp) = 3011.7;
    std::cout<< std::get<4>(tmp) << '\n';

    //方式3:通过原地索引API构造传值
    tmp = Mixtype(std::in_place_index<2>, {42, 74, 25, 36});
    for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
    std::cout << '\n';
    std::get<2>(tmp)[0] = 1024;  //对容器内的单个值进行修改
    for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
}

C++ 可变体(variant)_第6张图片

  • 方法4:emplace方法赋值。每个可变对象内置了emplace方法,下面是一个具体的例子:
    方法4的缺点是修改可变体内部容器对象时无法对单个元素的值做精准修改
#include


class ItCat {
public:
    ItCat()=default;
    ItCat(int, float) {}
};

int main()
{
    using Mixtype = std::variant<ItCat, int, std::vector<int>, std::string, double>;
    Mixtype tmp;


    //方式1:赋值操作符
    tmp=12;   //此时为int
    std::cout << tmp.index() << '\n';
    std::cout<< std::get<1>(tmp) << '\n';

    tmp = 23.5; //此时为double
    std::cout<< std::get<4>(tmp) << '\n';


    //方式2:通过get方法获取真正的对象,然后修改
    std::get<4>(tmp) = 3011.7;
    std::cout<< std::get<4>(tmp) << '\n';

    //方式3:通过原地索引API构造传值
    tmp = Mixtype(std::in_place_index<2>, {42, 74, 25, 36});
    for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
    std::cout << '\n';
    std::get<2>(tmp)[0] = 1024;  //对容器内的单个值进行修改
    for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
    std::cout << '\n';
    tmp.emplace<2>({0, 1, 2, 3, 4});  //替换下标为2的对象值
    for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
}

C++ 可变体(variant)_第7张图片

可变体的对象成员的生命周期

union无法支持其对象成员状态的自动化管理,因此必须手动调用构造函数或析构函数这很容易令程序员写出一大堆屎山代码。std::variant自动化解决对象成员的生命周期。 这意味着如果要切换当前存储对象的数据类型,则variant在切换类型之前,会调用底层类型的析构函数。下面这个示例,很好地解析了这些。

每次对可变体赋值,一旦赋值的数据类型会当前的数据类型不一致,可变体在赋值之前,它内部自动完成对当前持有的对象所占内存的垃圾回收。

std::variant 的访问者模式

std::variant 有一个重要的辅助函数接口 std::visit,这个API可以实现一个甚至多个可变体对象以引用的方式传递给,std::visit回调的函数,而这回调函数就是所谓的“访问者”,以实现一些非常复杂的业务逻辑。

下面是访问者模式的函数模板声明:

/**
 * @tparam Vistor      访问者函数,即visit的回调函数的函数指针
 * @tparam Variants    传入参数,一个或多个可变体对象的类型
 * @param visitor      访问者函数,即visit的回调函数
 * @param vars         传入参数,一个或多个可变体对象
 * @return constexpr auto 返回值
*/

template<class Vistor, class... Variants>
constexpr auto visit(Vistor&& visitor, Variants&&... vars);

visit的使用可以看如下例子:

#include

int main()
{
    std::variant<int, double, std::string> value = 1.123;
    static_assert(std::variant_size_v<decltype(value)> == 3, "error");
    std::visit(
        [](auto &&arg) {    //arg就是拿到的value中存的值
            //using C++17提供的重命名
            using T = std::decay_t<decltype(arg)>;  // 类型退化,去掉类型中的const 以及 &,拿到arg的类型
            if constexpr(std::is_same_v<T, int>) {     //编译时if,只有被选中的if constexpr分支才会被实例化。
                std::cout << "int: " << arg << '\n';
            } else if constexpr(std::is_same_v<T, double>) {   //std::is_same_v:判断输入的类型是否是指定的模板类型
                std::cout<< "double: "<< arg <<'\n';
            } else if constexpr(std::is_same_v<T, std::string>) {
                std::cout<< "string: "<< arg <<'\n';
            }
        }
        , value);

    return 0;
}

你可能感兴趣的:(C++面经,c++,开发语言)