【现代C++学习】variant 总结

参考来源:知乎_铁甲万能狗_C/C++ 修道院
参考来源:cppreference
参考来源:现代C++教程:第 4.3 节
参考来源:Runtime Polymorphism with std::variant and std::visit
参考来源:[C++] A C++17 Statemachine using std::tuple and std::variant

一、varaint 基础

varaint可以看做增强的Union变体,优点:

  • 对复杂类型的封装能力
  • 切换类型时,可以自动调用析构函数
  • 可变体不会发生额外的堆内存分配
  • 可以检查内部当前的活动类型

重要函数

  • index:返回当前可变体内部对应的数据类型的索引
    	std::variant<int, std::string> v = "abc";
        std::cout << "v.index = " << v.index() << '\n'; // v.index = 1
        v = {};
        std::cout << "v.index = " << v.index() << '\n'; // v.index = 0
    
  • std::get:获取可变体对应的数据类型的值
    std::variant<int, float> v{12}, w;
    std::cout << std::get<int>(v) << '\n'; // 12
    w = std::get<int>(v);
    w = std::get<0>(v);
    //  std::get<double>(v); // error: no double in [int, float]
    //  std::get<3>(v);      // error: valid index values are 0 and 1
    try {
    	w = 42.0f;
    	std::cout << std::get<float>(w) << '\n'; // ok, prints 42
    	w = 42;
    	std::cout << std::get<float>(w) << '\n'; // throws
    } catch (std::bad_variant_access const& ex) {
    	std::cout << ex.what() << ": w contained int, not float\n";
    } 
    
  • std::get_if:获取可变体对应的数据类型的值,在访问可变体时不会抛出bad_variant_access异常,提供了访问前的类型安全判断
    	auto check_value = [](const std::variant<int, float>& v) {
    		if(const int* pval = std::get_if<int>(&v)) {
    			std::cout << "variant value: " << *pval << '\n'; 
            } else {
            	std::cout << "failed to get value!" << '\n';
             }
        };
     
        std::variant<int, float> v{12}, w{3.f};
        check_value(v); // variant value: 12 
        check_value(w); // failed to get value!
    
  • std::holds_alternative:检查可变体对应类型是否是可切换的类型T
    	std::variant<int, std::string> v = "abc";
        std::cout << std::boolalpha << std::holds_alternative<int>(v) << '\n'; // flase
        std::cout << std::holds_alternative<std::string>(v) << '\n'; // true
    
  • std::variant_size_v:用于检测可变体内部可切换的数据类型的个数
    std::variant<int, float, std::string> v;
    static_assert(std::variant_size_v<decltype(v)> == 3);
    
  • std::visit:用于访问可变体中的当前处于活动状态的数据类型的实例
    // the variant to visit
    using var_t = std::variant<int, long, double, std::string>;
    
    // helper constant for the visitor #3
    template<class>
    inline constexpr bool always_false_v = false;
    
    int main()
    {
        std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
     
        for (auto& v: vec) {
            // 1. void visitor, only called for side-effects (here, for I/O)
            std::visit([](auto&& arg){ std::cout << arg; }, v);
     
            // 2. value-returning visitor, demonstrates the idiom of returning another variant
            var_t w = std::visit([](auto&& arg) -> var_t { return arg + arg; }, v);
     
            // 3. type-matching visitor: a lambda that handles each type differently
            std::cout << ". After doubling, variant holds ";
            std::visit([](auto&& arg) {
                using T = std::decay_t<decltype(arg)>;
                if constexpr (std::is_same_v<T, int>)
                    std::cout << "int with value " << arg << '\n';
                else if constexpr (std::is_same_v<T, long>)
                    std::cout << "long with value " << arg << '\n';
                else if constexpr (std::is_same_v<T, double>)
                    std::cout << "double with value " << arg << '\n';
                else if constexpr (std::is_same_v<T, std::string>)
                    std::cout << "std::string with value " << std::quoted(arg) << '\n';
                else 
                    static_assert(always_false_v<T>, "non-exhaustive visitor!");
            }, w);
        }
        /* 输出:
    	    10. After doubling, variant holds int with value 20
    		15. After doubling, variant holds long with value 30
    		1.5. After doubling, variant holds double with value 3
    		hello. After doubling, variant holds std::string with value "hellohello"
    	*/
    }
    

二、variant 初始化

  • 默认为第一个类型
    std::variant<std::string, double, float, int> v;
    std::cout << v.index() << "\n"; // 0
    
  • 如果可变体的第一个类型是聚合类型,则必须要有默认构造函数;否则可以没有,编译通过
    class Item {
    public:
    	Item(int, float) {}
    };
    
    class Items {
    public:
    	Items() = default;
    	Items(int, float) {}
    };
    
    int main()
    {
    	// std::variant<Item, double, std::string, int> v1;
    	// No matching constructor for initialization of 
    	// 'std::variant<Item, double, std::string, int>' 
    	// (aka 'variant<Item, double, basic_string<char, char_traits<char>, allocator<char>>, int>')
    	std::variant<std::string, double, Item, int> v2;
    	std::cout << v2.index() << "\n"; // 0
    	v2 = Item(1, 2.);
    	std::cout << v2.index() << "\n"; // 0
    	std::variant<Items, double, std::string, int> v3;
    	std::cout << v3.index() << "\n"; // 0
    	return 0;
    }
    
  • 类型模糊的传参
    std::variant<std::string, double, float, int> v = 12.; // 目标是 float, 但编译器将匹配数据类型的最大精度: double
    std::cout << v.index() << "\n"; // 1
    std::variant<std::string, double, float, int> v = 12.f;
    std::cout << v.index() << "\n"; // 2
    
  • 显式调用std::in_place_index告知可变体对象要在内置启用哪一个数据类型来构造可变体对象的实例
    std::tuple<std::string, double, int> t("123", 4.5, 8);
    std::variant<std::string, double, int> v {std::in_place_index<1>, std::get<1>(t)};
    std::variant<std::string, double, int> vs {std::in_place_index<0>, "123"};
    

三、修改可变体的对象成员的四种方式

  • 方式1:赋值操作符
  • 方式2:通过get方法获取真正的对象,然后修改
  • 方式3:通过原地索引API匹配数据类型,然后构造传值达到修改值的目的
  • 方式4:emplace方法赋值
class Item {
public:
	Item(int, float) {}
};

int main() {
	using MixType = std::variant<int, float, std::vector<int>, std::string, Item>;
	MixType v;

	// 方式1: 赋值操作符
	v= 12;
	std::cout << std::get<0>(v) << "\n"; // 12 
	// 方式2: 通过 get 方法获取真正的对象, 然后修改
	v = 23.5f;
	std::cout << std::get<1>(v) << "\n"; // 23.5 
	// 方式3: 通过原地索引 API 匹配数据类型, 然后构造传值达到修改值的目的
	v = MixType(std::in_place_index<2>, { 1, 2, 3 });
	std::cout << std::get<2>(v).size() << "\n"; // 3
	// 方式4: emplace 方法赋值
	v.emplace<3>("hello world!");
	std::cout << std::get<3>(v) << "\n"; // hello world!
	return 0;
} 

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

class Items {
public:
	Items() = default;
	Items(int, float) {}
	~Items() { std::cout << "Deconstruct!\n"; }
};
int main()
{
	using MixType = std::variant<int, float, std::vector<int>, std::string, Items>;
	MixType v;
	v.emplace<4>(1, 2.); // Deconstruct!
	v = 1;
    std::cout << std::get<0>(v) << "\n"; // 1
}

五、可变体的visit进阶

5.1 实现元组的运行期索引

template <size_t n, typename... T>
constexpr std::variant<T...> _tuple_index(const std::tuple<T...>& tpl, size_t i)
{
	if constexpr (n >= sizeof...(T)) {
		// throw std::out_of_range(" 越界.");
		exit(-1);
	}
	if (i == n) {
		return std::variant<T...>{ std::in_place_index<n>, std::get<n>(tpl) };
	}
	return _tuple_index<(n < sizeof...(T) - 1 ? n + 1 : 0)>(tpl, i);
}

template <typename... T>
constexpr std::variant<T...> tuple_index(const std::tuple<T...>& tpl, size_t i)
{
	return _tuple_index<0>(tpl, i);
}

template <typename... Ts>
std::ostream & operator<< (std::ostream & s, std::variant<Ts...> const & v)
{
	std::visit([&](auto && x){ s << x;}, v);
	return s;
}

int main() {
	std::tuple<std::string, double, int> t("123", 4.5, 8);
    std::cout << tuple_index(t, 0) << "\n"; // 123
    std::cout << tuple_index(t, 1) << "\n"; // 4.5
    std::cout << tuple_index(t, 2) << "\n"; // 8
    return 0;
}

5.2 访问模式

通过实现一个甚至多个可变体对象以引用的方式传递给std::visit回调的函数进行访问

1. 简单的访问模式

int main()
{
	auto say = [] (const auto& t) {
        std::cout << t << ", nice to meet you!" << "\n";
    };
    std::variant<int, float, std::string> man = {"Bob"};
    std::visit(say, man); // Bob, nice to meet you!
}

2. 复杂的访问模式

class CalculatorVisitor {
public:
    float mFactor;
    explicit CalculatorVisitor (float factor) : mFactor(factor)
    {}
    void operator()(int& i) const
    {
        i *= static_cast<int>(mFactor);
    }
    void operator()(float & i) const
    {
        i *= static_cast<float>(mFactor);
    }
    void operator()(std::vector<int> & arr) const
    {
        for (int k : arr) {
            k *= 2;
        }
    }
};
int main()
{
	std::variant<int, float, std::vector<int>> v = 2.5f;
    std::visit(CalculatorVisitor(0.97f), v);
    std::cout << std::get<1>(v) << "\n"; // 2.425
	return 0;
}

调用过程:

  1. 第一步:传值调用CalculatorVisitor进行实例化
  2. 第二步:可变体对象传递给std::visit函数
    • 查看可变体内部的活动对象的数据类型
    • 访问可变体对象内部的活动对象
  3. 第三步:将可变体活动对象的值传递给CalculatorVisitor实例
    这次传递不会重复初始化,std::visit调用了访问者CalculatorVisitor的构造函数,意味着std::visit方法持有了该访问者对象public公开的所有成员函数的上下文的控制权,因此std::visit可以要求CalculatorVisitor实现不同的行为
  4. 第四步:std::visit通过匹配不同重载版本的operator()运算符的函数指针

3. 模板访问

// the variant to visit
using var_t = std::variant<int, long, double, std::string>;

// helper type for the visitor
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };

// explicit deduction guide (not needed as of C++20)
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

int main()
{
	std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
	for (auto& v: vec) {
		// 4. another type-matching visitor: a class with 3 overloaded operator()'s
		// Note: The `(auto arg)` template operator() will bind to `int` and `long`
		//       in this case, but in its absence the `(double arg)` operator()
		//       *will also* bind to `int` and `long` because both are implicitly
		//       convertible to double. When using this form, care has to be taken
		//       that implicit conversions are handled correctly.
		std::visit(overloaded {
			[](auto arg) { std::cout << arg << ' '; },
			[](double arg) { std::cout << std::fixed << arg << ' '; },
			[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; }
			}, v);
	}
	// 输出: 10 15 1.500000 "hello"
	return 0;
}	 
int main()
{
	std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
    for (auto& vs: vec) {
        std::visit(overloaded {
            [](auto && value) {
                if constexpr (std::is_same_v<decltype(value), int&>) {
                    value = 2 * value + 1;
                    std::cout << value << ' ';
                } else if constexpr (std::is_same_v<decltype(value), double&>) {
                    value = 3 * 1.5f * (value - 1);
                    std::cout << value << ' ';
                } else if constexpr (std::is_same_v<decltype(value), std::string&>) {
                    std::cout << "\"" << value << "\"\n";
                }
            }
        }, vs);
    }
	// 输出: 21 2.25 "hello"
	return 0;
}

六、variant 的应用

6.1 简化树形数据结构的构建

class Node;
using Int = int;
using Float = float;
using Double = double;
using String = std::string;
using Null = std::monostate;
using NodeList = std::vector<Node>;
using Data = std::variant<Int, Float, Double, String, Null, NodeList>;

class Node {
public:
    std::string mName;
    Data mData;
    Node(std::string&& name, Data&& data) : mName(name), mData(data) {}
};

template<typename Stream>
Stream& operator<<(Stream& stream, const Null& null)
{
    stream << "null";
    return stream;
}

template<typename Stream>
Stream& operator<<(Stream& stream, const NodeList& data)
{
    std::cout << "[ ";
    for (const auto& d : data) {
        std::cout << d << ",";
    }
    std::cout << ']';
    return stream;
}
template<typename Stream>
Stream& operator<<(Stream& stream, Data& data)
{
    std::visit([&](auto& val) {
        stream << val;
    }, data);
    return stream;
}

template<typename Stream>
Stream& operator<<(Stream& stream, const Node& node)
{
    std::cout << "Node { name='" << node.mName << "', data=" << node.mData << " }";
    return stream;
}

struct Person{
    std::string name;
    int age;
};

template<typename T, typename Fn>
NodeList ToTree(const std::vector<T>& list, Fn callback)
{
    NodeList result;
    for (const auto& v: list) {
        result.push_back(callback(v));
    }
    return result;
}

int main()
{
    auto root = Node(
        "Match", NodeList({
            Node("TeamA", NodeList({
                Node("AA", 72),
                Node("AB", 55),
                Node("AC", 69)
            })),
            Node("TeamB", NodeList({
                Node("BA", 80),
                Node("BB", 57),
                Node("BC", 64)
            }))
        })
    );
    std::cout << root << "\n";

    std::vector<Person> vp = { { "Bob", 10 }, { "Jane", 22 } };
    auto pRoot = ToTree(vp, [] (auto&& arg) {
        std::string name = arg.name;
        Node x(std::move(name), arg.age);
        return x;
    });
    std::cout << pRoot << "\n";
    
    return 0;
}

6.2 实现运行时多态

// C++20
template <typename T>
concept ILabel = requires(const T v) {
    {v.buildHtml()} -> std::convertible_to<std::string>;
};

// C++20
struct HtmlLabelBuilder {
    template <ILabel... Label>
    [[nodiscard]] std::string buildHtml(const std::variant<Label...>& label)
    {
        return std::visit(*this, label);
    }

    template <ILabel Label>
    std::string operator()(const Label& label) const
    {
        return label.buildHtml();
    }
};

struct SimpleLabel {
    explicit SimpleLabel(std::string str) : _str(std::move(str)) { }

    [[nodiscard]] std::string buildHtml() const {
        return "<p>" + _str + "</p>";
    }

    std::string _str;
};

struct DateLabel {
    explicit DateLabel(std::string dateStr) : _str(std::move(dateStr)) { }

    [[nodiscard]] std::string buildHtml() const {
        return "<p class=\"date\">Date: " + _str + "</p>";
    }

    std::string _str;
};

struct IconLabel {
    IconLabel(std::string str, std::string iconSrc) : _str(std::move(str)), _iconSrc(std::move(iconSrc)) { }

    [[nodiscard]] std::string buildHtml() const {
        return "<p><img src=\"" + _iconSrc + "\"/>" + _str + "</p>";
    }

    std::string _str;
    std::string _iconSrc;
};

struct HelloLabel {
    [[nodiscard]] std::string buildHtml() const {
        return "<p>Hello</p>";
    }
};

int main()
{
	using LabelVariant = std::variant<SimpleLabel, DateLabel, IconLabel, HelloLabel>;
	auto vecLabels = std::vector<LabelVariant>{};
	vecLabels.emplace_back(HelloLabel{});
	vecLabels.emplace_back(SimpleLabel{"Hello World"});
	vecLabels.emplace_back(DateLabel{"10th August 2020"});
	vecLabels.emplace_back(IconLabel{"Error", "error.png"});
	{
		std::string finalHTML;
		auto builder = HtmlLabelBuilder{};
		for (auto &label : vecLabels) {
			finalHTML += builder.buildHtml(label) + '\n';
		}
		std::cout << finalHTML;
    }
	{
	    std::string finalHTML;
	    auto callBuildHTML = [](auto& label) { return label.buildHtml(); };
	    for (auto &label : vecLabels) {
	        finalHTML += std::visit(callBuildHTML, label) + '\n';
	    }
	    std::cout << finalHTML;
	}
	{
		// C++20
	    std::string finalHTML;
	    auto builder = HtmlLabelBuilder{};
	    for (auto &label : vecLabels)
	        finalHTML += builder.buildHtml(label) + '\n';
	
	    std::cout << finalHTML;
	}
	{
		// C++20
	    std::string finalHTML;
	    auto caller = [](ILabel auto& l) -> std::string { return l.buildHtml(); };
	    for (auto &label : vecLabels)
	        finalHTML += std::visit(caller, label) + '\n';
	
	    std::cout << finalHTML;
	}
	return 0;
}

6.3 实现简单状态机

#include <tuple>
#include <variant>
#include <iostream>

template <typename... States>
class state_machine {
private:
    // the tuple m_states holds all states we'll define
    std::tuple<States...> m_states;
    // in this variant we hold a reference to the current state, it's initialized by the state at index 0
    std::variant<States*...> m_current_state{ &std::get<0>(m_states) };

public:
    // we can change a state by calling set state with a state type
    template <typename State>
    void set_state()
    {
        m_current_state = &std::get<State>(m_states);
    }

    // we can define certain events which call a dedicated state transition
    template <typename Event>
    void on_state_transition(const Event& event)
    {
        auto execute_on_call = [this, &event] (auto current_state) {
            current_state->on_state_transition(event).execute(*this);
        };
        // std::visit "visits" the current_state and calls the lambda with the current state
        // this means every possible state needs to implement the execute function inside the lambda
        std::visit(execute_on_call, m_current_state);
    }

    // we call on_update of each state also with std::visit like above
    void on_update()
    {
        auto execute_on_call = [] (auto current_state) {
            current_state->on_update();
        };
        std::visit(execute_on_call, m_current_state);
    }
};

// the state transition type, where the template represents the target state
template <typename State>
struct state_transition_to {
    // on execute we're setting the target state in our statemachine by calling execute()
    template <typename Statemachine>
    void execute(Statemachine& statemachine) {
        statemachine.template set_state<State>();
    }
};

// an invalid state transition which has an emptye execute() function
// we need this (guess what) for all transitions we won't support
struct invalid_state_transition {
    template <typename Machine>
    void execute(Machine&) { }
};

// specific state transition types we support
struct transition_to_run{};
struct transition_to_idle{};
struct any_other_transition{};

// state definitions
struct idle;
struct run;
struct any_state;

// idle state which implements the methods to call by the statemachine
struct idle {
    // regular on update call
    static void on_update() {
        std::cout << "still waiting \n";
    }

    // specific transition to run, where we return the concrete state transition to run
    // to distinguish different state transitions, we use an empty function argument here
    static state_transition_to<run> on_state_transition(const transition_to_run&) {
        std::cout << "leaving idle with transition to run \n";
        return {};
    }

    // a template function to indicate all non-supported state transitions.
    template<typename Transition>
    [[nodiscard]] invalid_state_transition on_state_transition(const Transition&) const {
        std::cout << "state transition: " <<  typeid(Transition).name() << " is not supported in idle! \n";
        return {};
    }
};


// the same for our run state
struct run
{
    static void on_update()
    {
        std::cout << "we are running! \n";
    }

    static state_transition_to<idle> on_state_transition(const transition_to_idle&)
    {
        std::cout << "leaving run with transition to idle \n";
        return {};
    }

    template<typename Transition>
    [[nodiscard]] invalid_state_transition on_state_transition(const Transition&) const
    {
        std::cout << "state transition: " <<  typeid(Transition).name() << " is not supported in run! \n";
        return {};
    }
};

struct any_state
{
    // updating any state here
    void on_update() const {
    }

    // transition to idle
    [[nodiscard]] static state_transition_to<idle> on_state_transition(const transition_to_idle&) {
        return {};
    }

    // invalid transition to run (just as an example, this would be invalid)
    [[nodiscard]] static invalid_state_transition on_state_transition(const transition_to_run&) {
        return {};
    }

    // invalid transition to itself
    static invalid_state_transition on_state_transition(const any_other_transition&) {
        return {};
    }

    // and a lot of other states would follow....
};

// the alias for our state machine with state idle and run
// the statemachin is initialized with idle
using example_state_machine = state_machine<idle, run, any_state>; // 不用 any_state 也可

int main()
{
    example_state_machine machine;

    // update a couple of times in idle
    machine.on_update();
    machine.on_update();
    machine.on_update();


    // something happende, we change state to run
    machine.on_state_transition(transition_to_run{});

    // update a couple of times in run
    machine.on_update();
    machine.on_update();
    machine.on_update();

    // just for demonstration here
    // call an invalid state transition
    machine.on_state_transition(transition_to_run{});
    machine.on_state_transition(transition_to_run{});

    // just a demonstration with any other state
    machine.on_state_transition(any_other_transition{});

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值