可变模板和tuple

概述

可变模板是 C++11 引入的一项新功能,可以在模板参数里表达不定个数和类型的参数。

  • 用于在通用工具模板中转发参数到另外一个函数
  • 用于在递归的模板中表达通用的情况(另外会有至少一个模板特化来表达边界情况)

转发

标准库里的 make_unique 的定义:

template <typename T, typename... Args>
inline unique_ptr<T> make_unique(Args&&... args)
{
    return unique_ptr<T>(new T(forward<Args>(args)...));   
}
  • typename... Args 声明了一系列的类型class... 或 typename... 表示后面的标识符代表了一系列的类型。
  • Args&&... args 声明了一系列的形参 args,其类型是 Args&&。
  • forward(args)... 会在编译时实际逐项展开 Args 和 args ,参数有多少项,展开后就是多少项。

它就可以把传递给自己的全部参数转发到模板参数类的构造函数上去。

在这种情况下通常会使用 std::forward,确保参数转发时仍然保持正确的左值或右值引用类型。

看下面的代码:

make_unique<vector<int>>(100, 1)

模板实例化之后:

template <>
inline unique_ptr<vector<int>> make_unique(int&& arg1, int&& arg2)
{
  	return unique_ptr<vector<int>>(new vector<int>(forward<int>(arg1),forward<int>(arg2)));    
}

forward(args)... 每一项可变模板参数都以同样形式展开。项数允许为零,在调用构造函数时没有任何参数。

递归

用可变模板来实现编译期递归。看下面的代码:

template <typename T>
constexpr auto sum(T t)
{
    return t;
}

template <typename T, typename V, typename... Args>
constexpr auto sum(T t, V v, Args... args)
{
    return sum(t + v, args...);
}

// cout << sum(1, 2, 3.5, 4); // 10.5

模板会依次展开:

sum(1 + 2, 3.5, 4);
sum(3 + 3.5, 4);
sum(6.5 + 4);
6.5 + 4;

注意可以不必使用相同的数据类型:只要这些数据之间可以应用 +。

函数复合

对于函数 F 和 G ,函数复合即 F(G(x)),为了方便,不用 x 而仅仅用 F 和 G 组成一个整体函数来用。

递归的终结情况,单个函数的组合:

inline auto compose()
{
    return [](auto&& x) -> decltype(auto)
    {
        return std::forward<decltype(x)>(x);
    };
}

template <typename F>
auto compose(F&& f)
{
    return[f = std::forward<F>(f)](auto&&... x) -> decltype(auto)
    {
        return f(std::forward<decltype(x)>(x)...);
    };
}

正常有组合的情况:

template <typename F, typename... Args>
auto compose(F&& f, Args&&... args)
{
    return[f = std::forward<F>(f), args...](auto&&... x) -> decltype(auto) 
    {
        return f(compose(std::move(args)...)(std::forward<decltype(x)>(x)...));
    };        
}

在这个模板里,返回一个 lambda 表达式,然后用 f 捕捉第一个函数对象,用 args... 捕捉后面的函数对象。用 args... 继续组合后面的部分,然后把结果传到 f 里面。

注意标准库里的 transform接口非函数式,无法组合——它要求参数给出输出位置的迭代器,会修改迭代器指向的内容,返回结果也只是单个的迭代器。

函数式的接口则期望不修改参数的内容,结果完全在返回值中。

这里用 fmap 函数模板,实现如下:

template <template <typename, typename>class OutContainer =  std::vector, typename F, class R>
auto fmap(F&& fun, R&& input)
{
    typedef std::decay_t<decltype(fun(*input.begin()))> res_type;
    OutContainer<res_type, allocator<res_type>> res;
    for (auto&& item : input) {
        res.push_back(fun(item));
    }
    return res;
}
// 对输入范围中每一项都平方的函数对象:
auto sqr = [](auto&& C)
{
    return fmap([](auto&& v) {return v * v; }, C);
};

// 求和函数对象
auto sum = [](auto&& C)
{
    return accumulate(C.begin(), C.end(), 0); // #include<numeric>
};
vector<int> v{ 2,3,4};
// 顺序右到左,先sqr,再sum
cout << compose(sum, sqr)(v); //29
// 交换会报错

tuple

在 C++ 里,要通用地用一个变量来表达多个值,用多元组tuple 模板。

tuple 是 C++98 里的 pair 类型的一般化,可以表达任意多个固定数量、固定类型的值的组合。

#include <algorithm>
#include <iostream>
#include <string>
#include <tuple>
#include <vector>
using namespace std;

using num_tuple = tuple<int, string, string>;

ostream& operator<<(ostream& os, const num_tuple& value)
{
    os << get<0>(value) << ','
        << get<1>(value) << ','
        << get<2>(value);
    return os;
}

int main()
{
    vector<num_tuple> vn{
      {1, "one",   "un"},
      {2, "two",   "deux"},
      {3, "three", "trois"},
      {4, "four",  "quatre"} };
    get<2>(vn[0]) = "une";
    sort(vn.begin(), vn.end(),
        [](auto&& x, auto&& y) {
        return get<2>(x) <
            get<2>(y);
    });

    for (auto&& value : vn) {
        cout << value << endl;
    }
    constexpr auto size = tuple_size_v<num_tuple>;
    cout << "Tuple size is " << size << endl;
}

输出:

2,two,deux 4,four,quatre 3,three,trois 1,one,une Tuple size is 3

  • tuple 的成员数量由尖括号里写的类型数量决定。
  • 可以使用 get 函数对 tuple 的内容进行读和写。(当一个类型在 tuple 中出现正好一次时,我们也可以传类型取内容,即,对我们上面的三元组,get 是合法的,get 则不是。)
  • 可以用 tuple_size_v (在编译期)取得多元组里面的项数。

生成从 0 到项数减一之间的整数序列。标准库里定义了相关的工具make_index_sequence,简化实现如下所示:

template <class T, T... Ints>
struct m_integer_sequence {};

template <size_t... Ints>
using m_index_sequence = m_integer_sequence<size_t, Ints...>;

template <size_t N, size_t... Ints>
struct m_index_sequence_helper
{
    typedef typename m_index_sequence_helper<N - 1, N - 1, Ints...>::type type;
};

template <size_t... Ints>
struct m_index_sequence_helper<0, Ints...>
{
    typedef m_index_sequence<Ints...> type;
};

template <size_t N>
using make_m_index_sequence = typename m_index_sequence_helper<N>::type;

make_m_index_sequence<N>结果是 m_integer_sequence<size_t, 0, 1, 2, ..., N - 1>

利用这个模板,写出标准库里的 apply 函数模板简化版本:

template <class F, class Tuple, size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Tuple&& t, m_index_sequence<I...>)
{
    return f(get<I>(forward<Tuple>(t))...);
}

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t)
{
    return apply_impl(forward<F>(f), forward<Tuple>(t),
        make_m_index_sequence<tuple_size_v<remove_reference_t<Tuple>>>{});
}

一个三元组 t,类型 tuple,去 apply 一个函数 f,展开后得到 apply_impl(f, t, m_index_sequence<0, 1, 2>{}),再展开后得到了有 get<0>、get<1>、get<2> 的函数调用形式。利用一个计数序列的类型,可以在编译时展开 tuple 里的各个成员,并用来调用函数。

数值预算

快速地计算一串二进制数中 1 比特的数量。

如果有十进制的 31 和 254,转换成二进制是 00011111 和 11111110,那得到 5 + 7 = 12。

利用 constexpr 函数,让编译器在编译时帮我们计算单个数值。

constexpr int count_bit(unsigned char value)
{
    if (value == 0) return 0;
    return (value & 1) + count_bit(value >> 1);
}

定义一个模板,它的参数是一个序列,在初始化时这个模板会对参数里的每一项计算比特数,并放到数组成员里。

template <size_t... V>
struct bit_count_t 
{
    unsigned char count[sizeof...(V)] = { static_cast<unsigned char>(count_bit(V))... };    
};

sizeof...(V) 可以获得参数的个数(在 tuple_size_v 的实现里实际也用到它了)。

如果模板参数传 0, 1, 2, 3,结果里面就会有个含 4 项元素的数组,数值分别是对 0、1、2、3 的比特计数。

利用 make_m_index_sequence 来展开计算了,想产生几项就可以产生几项。

要注意到 make_m_index_sequence 的结果是个类型,不能直接用在 bit_count_t 的构造中,用模板匹配中转一下。

template <size_t... V>
constexpr bit_count_t<V...>
get_bit_count(m_index_sequence<V...>)
{
    return bit_count_t<V...>();
}

auto bit_count = get_bit_count(make_m_index_sequence<256>());
   
int main()
{
    for (int i = 0; i < 16; ++i) {
        // 0 1 1 2 1 2 2 3 1 2 2 3 2 3 3 4
        cout << static_cast<unsigned>(bit_count.count[i]) << ' ';
    }
}