C++11 Std::tuple 和它的应用

模板类 std::tuple 是一个固定大小, 存储元素类型不同的集合. 它是 std::pair 的泛化版本.

一个 tuple 可以显示的声明它每个元素的类型, 也可以用 std::make_tuple 模板函数来实现自动类型推导. 可以用 std::get 指定索引来访问 tuple 中的元素. 如下:

1
2
3
4
5
6
std::tuple<string, int> t2("bitdewy", 123);

auto t = std::make_tuple(string("bitdewy"), 10, 1.23);   // t will be of type tuple<string, int, double>
std::string s = std::get<0>(t);
int x = std::get<1>(t);
double d = std::get<2>(t);

当编译期我们需要一个存放不同类型数据的集合, 但又不想定义一个具名的类时 tuple 是非常有用的. 例如 std::functionstd::bind 就使用 tuple 来存放参数(我们都知道 std::bind 从第二个参数开始, 就是函数的参数了, 参数个数是不定的, 类型也是不定的, 这太适合用 tuple 来定义以及存储函数参数列表了). 尤其是 C++11 开始支持变长模板参数了, 这样一来 tuple 就变得更方便了.

std::tie

很多时候我们都希望函数能够返回两个或者更多个值, std::tie 可以帮助我们解决这个问题. std::tie 会构造一个每个元素都是左值引用的 std::tuple. 所以当一个函数返回一个 std::tuple 时, 我们可以使用 std::tie 构造一个 std::tuple 来接收这些返回值. 同时, 如果我们的类的每个元素都支持比较的话, 我们还可以直接使用它来构造一个 std::tuple 来使用 std::tuple 的比较函数. 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string>
#include <set>
#include <tuple>

struct S {
    int n;
    std::string s;
    float d;
    bool operator<(const S& rhs) const {
        return std::tie(n, s, d) < std::tie(rhs.n, rhs.s, rhs.d);
    }
};

int main() {
    std::set<S> set_of_s; // S is LessThanComparable

    S value{ 42, "Test", 3.14 };
    std::set<S>::iterator iter;
    bool inserted;

    // unpacks the return value of insert into iter and inserted
    std::tie(iter, inserted) = set_of_s.insert(value);

    if (inserted) {
        std::cout << "Value was inserted successfully\n";
  }
  return 0;
}

tuple_visitor

visitor 模式是 GoF 书中描述的 23 种设计模式中最难懂的一个. 这个模式甚至让 Scott Meyers 都困惑了一阵, 显然这个模式的名字没有取好, 而且例子中继承过来继承过去的绕了很多道弯很容易就让你搞不清楚它到底是在做什么的了.

回顾一下我们熟悉的虚函数, 本质上虚函数的作用是在不改变行为的基础上可以任意扩展类型, 也就是说我们可以在不更改原有代码的情况下, 将新的类型插入到我们原有的系统中而不需要更改原有系统的代码.

而 visitor 模式只是从另一个角度进行了解耦, 本质上 visitor 模式的作用是在不改变类型的基础上可以任意扩展对类型的操作.

不明白上面两句话的可以看看这两篇如何设计一门语言(五)——面向对象和消息发送, 如何设计一门语言(十二)——设计可扩展的类型文章.

之前介绍过 boost.variant.static_visitor, 没有了一个一个的继承,写起来比原始的 visitor 模式简单很多, 如果写过 parser 生成过 AST 然后对它操作的话, 那么你可能对 visitor 有更深刻的理解, 本质上它就是函数式语言中含有模式匹配的递归函数.

模式匹配不是什么新玩意儿, 事实上, 它甚至和函数式编程的关系都不大. 把产生模式匹配归因于函数式编程的唯一的原因是函数式语言早就提供了模式匹配, 然而现在的命令式语言还大多做不到. C++ 中的模板特化实际上就是一种模式匹配(类型模式). 比如 std::enable_if 里面经常要用到的 type traits. 下面是一个最简单的模式匹配的例子:

1
2
3
4
5
6
7
8
9
10
11
template <int N>
struct Factorial {
  static const int value = N * Factorial<N - 1>::value;
};

// Base case via template specialization:

template <>
struct Factorial<0> {
  static const int value = 1;
};

这可能是每一个介绍模板元编程都要使用的一个例子, 它利用模式匹配成功的消灭了分支, 进行了编译期的运算, 当然这是个只能演示而没有什么实际意义的代码. 但是表达了模式匹配的意义,

回到正题, 继续我们的 visitor, 有时候我们有遍历 tuple 中存储元素的需求, 最简单的比如按顺序打印, 也许还有其他的针对每个元素的操作, 理论上这和 visitor 模式是类似的. 简单的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <tuple>
#include <iostream>

template<std::size_t> struct int_{};

template <typename Functor, typename Tuple>
void tuple_visitor_impl(Functor&& functor, const Tuple& t, int_<1>) {
  functor(std::get<std::tuple_size<Tuple>::value - 1>(t));
}

template <typename Functor, typename Tuple, size_t Pos>
void tuple_visitor_impl(Functor&& functor, const Tuple& t, int_<Pos>) {
  functor(std::get<std::tuple_size<Tuple>::value - Pos>(t));
  tuple_visitor_impl(std::forward<Functor&&>(functor), t, int_<Pos - 1>());
}

template <typename Functor, typename... Args>
void tuple_visitor(Functor&& functor, const std::tuple<Args...>& t) {
  tuple_visitor_impl(std::forward<Functor&&>(functor), t, int_<sizeof...(Args)>());
}

struct F {
  template <typename T>
  void operator()(T&& t) { std::cout << "unexpect type: " << typeid(std::forward<T&&>(t)).name() << std::endl; }
  void operator()(int i) { std::cout << "void F::operator()(int): " << i << std::endl; }
  void operator()(double d) { std::cout << "void F::operator()(double): " << d << std::endl; }
  void operator()(const std::string& s) { std::cout << "void F::operator()(const std::string&): " << s << std::endl; }

};

int main() {
  auto t = std::make_tuple(10, std::string("Test"), 3.14);
  F f;
  tuple_visitor(f, t);
  return 0;
}

当然 F 中要有针对每个类型的 operator(), 这样才能保证每个类型的 tuple 元素都能得到正确的处理, 当然由于我们有泛型版本的 operator() 所以任何类型都能正确接收, 只是行为不正确而已, 上面的代码中是打印出了数据类型, 更好的做法可能是抛出一个异常. 例子中的 operator() 都是重载, 实际上改为特化会更具通用性, 因为一旦改为特化, 那么 struct F 就可以变成框架内的细节, 可以做一些额外的工作. 当用户想要使用 tuple_visitor 的时候, 只需要针对 tuple 中的元素类型, 特化自己的 F::operator() 就可以了.

tuple_expander

还记得最上面介绍 tuple 的时候说的, std::bind 是用 tuple 来存储函数参数的吗? 那么由参数构造一个 tuple 是很显而易见的, 那么如何展开一个 tuple 呢? 这里有一个实现, 可以作为参考. snippet/utility/expander.hpp

参考

Comments