现代C++神器之variant

看过这两年的Cppcon都应该对variant印象深刻。说起来这个玩意也不算新,2002年就已经有了boost::variant

说到variant就得提起enumunionunion对非C系编程者可能有点陌生。按照当代角度,enum和union很大一部分作用在于缩减定义域。举例如下:

//choose one implementation below
enum OneTwoTree { ONE, TWO, TREE };
//
union OneTwoTree {
  int ONE;
  int TWO;
  int TREE;
};
//
void foo(OneTwoTree ott);

这里用OneTwoTree限制了相关领域的类型范围,比如说调用foo(4)时编译器就会报错。

variant相当于模板化的union,我认为它主要提供了形式上的便利。

struct One {
  int val = 1;
};
struct Two {
  int val = 2;
};
struct Tree {
  int val = 3;
};
using OneTwoTree = std::variant<One, Two, Tree>;
auto getOneTwoTree(const OneTwoTree& ott) {
  struct {
    auto operator()(const One& n) { return n.val; }
    auto operator()(const Two& n) { return n.val; }
    auto operator()(const Tree& n) { return n.val; }
  } visitor;
  return std::visit(visitor, ott);
}
//
One n1;
Two n2;
Tree n3;
cout << getOneTwoTree(n1) << endl; //output 1
cout << getOneTwoTree(n2) << endl; //output 2
cout << getOneTwoTree(n3) << endl; //output 3

应该能看出来,这是个Visitor Pattern。C++17也钦定了std::visit这个函数,可以说相当好用。(其实要更方便还可以再封装一层,但不在本次讨论之列)。

理论的东西网上有很多,这里说点实际操作,不一定有用,但很有趣。之前的文章我简述过State Pattern,它巧妙的地方在于每个转换函数都会返回下一个目标状态,这个技巧也能用在这里。接下来搞个最简单的例子,一个走走停停的状态机,只有IdleMoving两种状态和stoprun两种动作。状态的实现当然就是用variant了:

struct Idle {};
struct Moving {};
using State = std::variant<Idle, Moving>;

动作和之前的例子相同,就由一个带visitor的函数表示:

auto stop(const State& s) {
  struct {
    State operator()(const Idle& x) const { return Idle{}; }
    State operator()(const Moving& x) const { return Idle{}; }
  } stopVisitor;
  return std::visit(stopVisitor, s);
}
auto run(const State& s) {
  struct {
    State operator()(const Idle& x) const { return Moving{}; }
    State operator()(const Moving& x) const { return Moving{}; }
  } runVisitor;
  return std::visit(runVisitor, s);
}

这个实现看起来至少比State Pattern要简单很多,而且形式上还有优化空间。

我个人觉得std::variant有点改变规则的味道。加上std::visit,可以重新编排很多旧设计。

P.S. 现在linux下,libstdc++的std::variant还有bug,上面的写法编译会报错,需要切换libc++。MSVC我没试过。

Leave a Reply

Your email address will not be published. Required fields are marked *