Boost C++ 库

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

简单的 C++11 元编程 - Boost C++ 函数库

借助可变模板、参数包和模板别名

注意
我写这篇文章的灵感来自于我阅读了 Eric Niebler 颇具启发性的 Tiny Metaprogramming Library 文章。感谢 Eric。

C++11 改变了游戏规则

Boost.MPL 的广泛接受使得 C++ 元编程看起来已经是一个已解决的问题。也许 MPL 并非完美,但它已经足够好,以至于没有真正寻求或产生替代方案的必要。

C++11 改变了游戏规则。可变模板及其相关的参数包的加入,直接将编译时类型列表结构引入了语言。而以前每个元编程库都定义了自己的类型列表,MPL 定义了好几个,但在 C++11 中,类型列表就像

// C++11
template<class... T> struct type_list {};

几乎没有理由使用其他任何东西。

模板别名是另一个改变游戏规则的特性。以前,"元函数",即接受一个类型并产生另一个类型的模板,看起来像

// C++03
template<class T> struct add_pointer { typedef T* type; };

并且被这样使用

// C++03
typedef typename add_pointer<X>::type Xp;

在 C++11 中,元函数可以是模板别名,而不是类模板

// C++11
template<class T> using add_pointer = T*;

上面的示例用法则变为

// C++11
typedef add_pointer<X> Xp;

或者,如果你更喜欢被看作是 C++11 达人,

// C++11
using Xp = add_pointer<X>;

这对于更复杂的表达式来说是一个显著的改进

// C++03
typedef
    typename add_reference<
        typename add_const<
            typename add_pointer<X>::type
        >::type
    >::type Xpcr;
// C++11
using Xpcr = add_reference<add_const<add_pointer<X>>>;

(该示例还利用了另一个 C++11 特性——你现在可以使用 >> 来关闭模板,而不会被解释为右移。)

此外,模板别名可以传递给模板模板参数

// C++11
template<template<class... T> class F> struct X
{
};

X<add_pointer>; // works!

这些语言改进使得 C++11 元编程与 C++03 的惯用写法有显著不同。Boost.MPL 不再足够好,必须采取行动。但做什么呢?

类型列表和 mp_rename

让我们从基础开始。我们的基本数据结构将是类型列表

template<class... T> struct mp_list {};

为什么是 mp_ 前缀? mp 显然代表元编程,但我们不能使用命名空间吗?

确实可以。但 Boost.MPL 的过往经验表明,我们的元编程原语与标准标识符(如 list)和关键字(如 ifinttrue)之间的名称冲突将很常见,并且会成为问题的根源。使用前缀,我们就可以避免所有这些麻烦。

所以我们有了类型列表,并可以往里面放东西

using list = mp_list<int, char, float, double, void>;

但目前还不能做其他任何事情。我们需要一个操作 mp_list 的原语库。但在深入研究之前,我们先考虑另一个有趣的问题。

假设我们有了一个可以处理 mp_list 的原语库,但其他代码给了我们一个非 mp_list 的类型列表,例如 std::tuple<int, float, void*>,或者 std::packer<int, float, void*>

假设我们需要以某种方式修改这个外部类型列表(比如将类型转换为指针),并将其转换后的结果以接收到的形式返回,在第一种情况下是 std::tuple<int*, float*, void**>,在第二种情况下是 std::packer<int*, float*, void**>

要做到这一点,我们首先需要将 std::tuple<int, float, void*> 转换为 mp_list<int, float, void*>,将 add_pointer 应用于每个元素得到 mp_list<int*, float*, void**>,然后将其转换回 std::tuple

这些转换步骤是相当常见的,我们将编写一个帮助我们执行这些转换的原语,称为 mp_rename。我们希望

mp_rename<std::tuple<int, float, void*>, mp_list>

得到

mp_list<int, float, void*>

反之亦然,

mp_rename<mp_list<int, float, void*>, std::tuple>

得到

std::tuple<int, float, void*>

这是 mp_rename 的实现

template<class A, template<class...> class B> struct mp_rename_impl;

template<template<class...> class A, class... T, template<class...> class B>
    struct mp_rename_impl<A<T...>, B>
{
    using type = B<T...>;
};

template<class A, template<class...> class B>
    using mp_rename = typename mp_rename_impl<A, B>::type;

(这种模板别名转发到执行实际工作的类模板的模式很常见;类模板可以特化,而模板别名则不能。)

请注意,mp_rename 不会特殊对待任何列表类型,甚至不是 mp_list;它可以将任何可变类模板重命名为任何其他模板。你可以用它将 std::packer 重命名为 std::tuple,再重命名为 std::variant(一旦出现这样的东西),它都会欣然照办。

事实上,它甚至可以重命名非可变类模板,如下例所示

mp_rename<std::pair<int, float>, std::tuple>        // -> std::tuple<int, float>
mp_rename<mp_list<int, float>, std::pair>           // -> std::pair<int, float>
mp_rename<std::shared_ptr<int>, std::unique_ptr>    // -> std::unique_ptr<int>

魔法是有限制的;unique_ptr 不能重命名为 shared_ptr

mp_rename<std::unique_ptr<int>, std::shared_ptr>    // error

因为 unique_ptr<int> 实际上是 unique_ptr<int, std::default_delete<int>>,而 mp_rename 会将其重命名为 shared_ptr<int, std::default_delete<int>>,这不会编译。但它仍然在比人们最初朴素期望的更多情况下有效。

转换不再是问题,让我们继续研究原语,并定义一个简单的练习,mp_size。我们希望 mp_size<mp_list<T...>> 给出列表中元素的数量,即表达式 sizeof...(T) 的值。

template<class L> struct mp_size_impl;

template<class... T> struct mp_size_impl<mp_list<T...>>
{
    using type = std::integral_constant<std::size_t, sizeof...(T)>;
};

template<class L> using mp_size = typename mp_size_impl<L>::type;

这相对简单,除了 std::integral_constant。它是什么,为什么我们需要它?

std::integral_constant 是一个标准的 C++11 类型,它将一个整数常量(即编译时常量整数值)包装成一个类型。

由于元编程操作的是只能容纳类型的类型列表,因此将编译时常量表示为类型很方便。这允许我们以统一的方式处理类型列表和值列表。因此,在元编程中,接受和返回类型而不是值是惯用的,我们也是这样做的。如果稍后我们需要实际值,我们可以使用表达式 mp_size<L>::value 来检索它。

现在我们有了 mp_size,但你可能已经注意到 mp_sizemp_rename 之间有一个有趣的差异。而我特意强调 mp_rename 不将 mp_list 视为特殊情况,mp_size 则非常明确地将其视为特殊情况

template<class... T> struct mp_size_impl<mp_list<T...>>

这真的有必要吗?我们不能在 mp_size 的实现中使用与 mp_rename 相同的技术吗?

template<class L> struct mp_size_impl;

template<template<class...> class L, class... T> struct mp_size_impl<L<T...>>
{
    using type = std::integral_constant<std::size_t, sizeof...(T)>;
};

template<class L> using mp_size = typename mp_size_impl<L>::type;

是的,我们绝对可以,这个改进使我们能够对任何其他类型列表使用 mp_size,例如 std::tuple。它将 mp_size 变成一个真正的通用原语。

这很好。它好到我敢说我们所有的元编程原语都应该具有这个属性。如果有人以 std::tuple 的形式给我们一个类型列表,我们应该能够直接对其进行操作,避免了与 mp_list 的相互转换。

那么我们是否不再需要 mp_rename 了?并非如此。除了有时我们确实需要重命名类型列表的事实之外,mp_rename 还有另一个令人惊讶的用途。

为了说明这一点,我将介绍原语 mp_length。它类似于 mp_size,但 mp_size 接受类型列表作为参数,而 mp_length 接受可变参数包并返回其长度;或者,换句话说,它返回其参数的数量

template<class... T> using mp_length =
    std::integral_constant<std::size_t, sizeof...(T)>;

我们如何实现 mp_sizemp_length 为基础?一种选择是直接替换后者的实现

template<template<class...> class L, class... T> struct mp_size_impl<L<T...>>
{
    using type = mp_length<T...>;
};

但还有另一种方式,远不那么平凡。想想 mp_size 的作用。它接受参数

mp_list<int, void, float>

并返回

mp_length<int, void, float>

我们已经有了一个类似的原语了吗?

(没太多选择,是吧?)

确实有,它叫做 mp_rename

template<class L> using mp_size = mp_rename<L, mp_length>;

我不知道你怎么样,但我发现这个技术很迷人。它利用了列表 L<T...> 和元函数 "调用" F<T...> 之间的结构相似性,以及语言以相同方式看待这些事物,并允许我们将模板别名 mp_length 传递给 mp_rename,就好像它是一个普通的类模板,如 mp_list 一样。

(其他元编程库提供了一个专门的 apply 原语来完成这项工作。apply<F, L> 用列表 L 的内容调用元函数 F。我们将添加一个别名 mp_apply<F, L>,它调用 mp_rename<L, F> 以提高可读性。)

template<template<class...> class F, class L> using mp_apply = mp_rename<L, F>;

mp_transform

让我们回顾一下我之前给出的例子——有人给了我们 std::tuple<X, Y, Z>,我们需要计算 std::tuple<X*, Y*, Z*>。我们已经有了 add_pointer

template<class T> using add_pointer = T*;

所以我们只需要将其应用于输入元组的每个元素。

将一个函数和一个列表结合起来并将其应用于每个元素的算法在 Boost.MPL 和 STL 中称为 transform,在函数式语言中称为 map。为了与现有的 C++ 实践保持一致(map 在 STL 和 Boost.MPL 中都是一个数据结构),我们将使用 transform

我们将把我们的算法称为 mp_transform,而 mp_transform<F, L> 将把 F 应用于 L 的每个元素并返回结果。通常,参数顺序是颠倒的,函数放在最后。我们将其放在前面的原因稍后会显现。

有许多方法可以实现 mp_transform;我们将选择的一种方法将利用另一个原语,mp_push_frontmp_push_front<L, T>,顾名思义,将 T 添加为 L 的第一个元素

template<class L, class T> struct mp_push_front_impl;

template<template<class...> class L, class... U, class T>
    struct mp_push_front_impl<L<U...>, T>
{
    using type = L<T, U...>;
};

template<class L, class T>
    using mp_push_front = typename mp_push_front_impl<L, T>::type;

但没有理由将 mp_push_front 限制为单个元素。在 C++11 中,可变模板应该是我们的默认选择,并且能够接受任意数量元素的 mp_push_front 实现几乎是相同的

template<class L, class... T> struct mp_push_front_impl;

template<template<class...> class L, class... U, class... T>
    struct mp_push_front_impl<L<U...>, T...>
{
    using type = L<T..., U...>;
};

template<class L, class... T>
    using mp_push_front = typename mp_push_front_impl<L, T...>::type;

继续 mp_transform

template<template<class...> class F, class L> struct mp_transform_impl;

template<template<class...> class F, class L>
    using mp_transform = typename mp_transform_impl<F, L>::type;

template<template<class...> class F, template<class...> class L>
    struct mp_transform_impl<F, L<>>
{
    using type = L<>;
};

template<template<class...> class F, template<class...> class L, class T1, class... T>
    struct mp_transform_impl<F, L<T1, T...>>
{
    using _first = F<T1>;
    using _rest = mp_transform<F, L<T...>>;

    using type = mp_push_front<_rest, _first>;
};

这是一个直接的递归实现,对于有函数式编程背景的人来说应该很熟悉。

我们可以做得更好吗?事实证明,在 C++11 中,我们可以。

template<template<class...> class F, class L> struct mp_transform_impl;

template<template<class...> class F, class L>
    using mp_transform = typename mp_transform_impl<F, L>::type;

template<template<class...> class F, template<class...> class L, class... T>
    struct mp_transform_impl<F, L<T...>>
{
    using type = L<F<T>...>;
};

在这里,我们利用了包展开内置于语言的事实,因此 F<T>... 部分为我们完成了所有迭代工作。

我们现在可以解决我们最初的挑战:给定一个类型的 std::tuple,返回一个指向这些类型的指针的 std::tuple

using input = std::tuple<int, void, float>;
using expected = std::tuple<int*, void*, float*>;

using result = mp_transform<add_pointer, input>;

static_assert( std::is_same<result, expected>::value, "" );

mp_transform,第二部分

如果我们有一个元组对作为输入,并且需要产生相应的元组对呢?例如,给定

using input = std::pair<std::tuple<X1, X2, X3>, std::tuple<Y1, Y2, Y3>>;

我们需要产生

using expected = std::tuple<std::pair<X1, Y1>, std::pair<X2, Y2>, std::pair<X3, Y3>>;

我们需要取两个列表,由输入中的元组表示,并通过使用 std::pair 将它们成对组合。如果我们把 std::pair 看作一个函数 F,这个任务就非常类似于 mp_transform,不同之处在于我们需要使用一个二元函数和两个列表。

将我们的一元变换算法改为二元算法并不难

template<template<class...> class F, class L1, class L2>
    struct mp_transform2_impl;

template<template<class...> class F, class L1, class L2>
    using mp_transform2 = typename mp_transform2_impl<F, L1, L2>::type;

template<template<class...> class F,
    template<class...> class L1, class... T1,
    template<class...> class L2, class... T2>
    struct mp_transform2_impl<F, L1<T1...>, L2<T2...>>
{
    static_assert( sizeof...(T1) == sizeof...(T2),
        "The arguments of mp_transform2 should be of the same size" );

    using type = L1<F<T1,T2>...>;
};

我们现在可以做到

using input = std::pair<std::tuple<X1, X2, X3>, std::tuple<Y1, Y2, Y3>>;
using expected = std::tuple<std::pair<X1, Y1>, std::pair<X2, Y2>, std::pair<X3, Y3>>;

using result = mp_transform2<std::pair, input::first_type, input::second_type>;

static_assert( std::is_same<result, expected>::value, "" );

再次利用元函数和普通类模板(如 std::pair)之间的相似性,这次是另一个方向;我们将 std::pair 传递给 mp_transform2,后者期望一个元函数。

我们是否必须为每个元数使用单独的变换算法?如果我们想要一个接受三元函数和三个列表的变换算法,我们应该称之为 mp_transform3 吗?不,这正是我们把函数放在前面的原因。我们只需要将 mp_transform 改为可变

template<template<class...> class F, class... L> struct mp_transform_impl;

template<template<class...> class F, class... L>
    using mp_transform = typename mp_transform_impl<F, L...>::type;

然后添加一元和二元特化

template<template<class...> class F, template<class...> class L, class... T>
    struct mp_transform_impl<F, L<T...>>
{
    using type = L<F<T>...>;
};

template<template<class...> class F,
    template<class...> class L1, class... T1,
    template<class...> class L2, class... T2>
    struct mp_transform_impl<F, L1<T1...>, L2<T2...>>
{
    static_assert( sizeof...(T1) == sizeof...(T2),
        "The arguments of mp_transform should be of the same size" );

    using type = L1<F<T1,T2>...>;
};

我们还可以添加三元及更多特化。

是否有可能实现真正可变的 mp_transform,一个可以处理任意数量列表的?原则上是可以的,我在这里提供一个可能的简化实现以示完整性

template<template<class...> class F, class E, class... L>
    struct mp_transform_impl;

template<template<class...> class F, class... L>
    using mp_transform = typename mp_transform_impl<F, mp_empty<L...>, L...>::type;

template<template<class...> class F, class L1, class... L>
    struct mp_transform_impl<F, mp_true, L1, L...>
{
    using type = mp_clear<L1>;
};

template<template<class...> class F, class... L>
    struct mp_transform_impl<F, mp_false, L...>
{
    using _first = F< typename mp_front_impl<L>::type... >;
    using _rest = mp_transform< F, typename mp_pop_front_impl<L>::type... >;

    using type = mp_push_front<_rest, _first>;
};

但将省略它使用的原语。这些是

  • mp_true——std::integral_constant<bool, true> 的别名。

  • mp_false——std::integral_constant<bool, false> 的别名。

  • mp_empty<L...>——如果所有列表都为空,则返回 mp_true,否则返回 mp_false

  • mp_clear<L>——返回一个与 L 类型相同的空列表。

  • mp_front<L>——返回 L 的第一个元素。

  • mp_pop_front<L>——返回不包含第一个元素的 L

递归 mp_transform 实现与基于语言的实现之间有一个有趣的区别。mp_transform<add_pointer, std::pair<int, float>> 可以与 F<T>... 实现一起工作,而与递归实现一起则会失败,因为 std::pair 不是真正的类型列表,并且只能容纳恰好两个类型。

臭名昭著的 tuple_cat 挑战

Eric Niebler 在他的 Tiny Metaprogramming Library 文章中,将函数 std::tuple_cat 作为一个元编程挑战。tuple_cat 是一个可变模板函数,它接受多个元组并将它们连接成另一个 std::tuple。这是 Eric 的解决方案

namespace detail
{
    template<typename Ret, typename...Is, typename ...Ks,
        typename Tuples>
    Ret tuple_cat_(typelist<Is...>, typelist<Ks...>,
        Tuples tpls)
    {
        return Ret{std::get<Ks::value>(
            std::get<Is::value>(tpls))...};
    }
}

template<typename...Tuples,
    typename Res =
        typelist_apply_t<
            meta_quote<std::tuple>,
            typelist_cat_t<typelist<as_typelist_t<Tuples>...> > > >
Res tuple_cat(Tuples &&... tpls)
{
    static constexpr std::size_t N = sizeof...(Tuples);
    // E.g. [0,0,0,2,2,2,3,3]
    using inner =
        typelist_cat_t<
            typelist_transform_t<
                typelist<as_typelist_t<Tuples>...>,
                typelist_transform_t<
                    as_typelist_t<make_index_sequence<N> >,
                    meta_quote<meta_always> >,
                meta_quote<typelist_transform_t> > >;
    // E.g. [0,1,2,0,1,2,0,1]
    using outer =
        typelist_cat_t<
            typelist_transform_t<
                typelist<as_typelist_t<Tuples>...>,
                meta_compose<
                    meta_quote<as_typelist_t>,
                    meta_quote_i<std::size_t, make_index_sequence>,
                    meta_quote<typelist_size_t> > > >;
    return detail::tuple_cat_<Res>(
        inner{},
        outer{},
        std::forward_as_tuple(std::forward<Tuples>(tpls)...));
}

好的,接受挑战。让我们看看我们能做什么。

正如 Eric 所解释的,这个实现依赖于一个巧妙的技巧:将输入元组打包成一个元组,创建两个索引数组 innerouter,然后用外部索引和结果(其中一个输入元组)来索引外部元组,再用内部索引来索引它。

因此,例如,如果 tuple_cat 被调用为

std::tuple<int, short, long> t1;
std::tuple<> t2;
std::tuple<float, double, long double> t3;
std::tuple<void*, char*> t4;

auto res = tuple_cat(t1, t2, t3, t4);

我们将创建元组

std::tuple<std::tuple<int, short, long>, std::tuple<>,
    std::tuple<float, double, long double>, std::tuple<void*, char*>> t{t1, t2, t3, t4};

然后通过以下方式提取 t 的元素

std::get<0>(std::get<0>(t)), // t1[0]
std::get<1>(std::get<0>(t)), // t1[1]
std::get<2>(std::get<0>(t)), // t1[2]
std::get<0>(std::get<2>(t)), // t3[0]
std::get<1>(std::get<2>(t)), // t3[1]
std::get<2>(std::get<2>(t)), // t3[2]
std::get<0>(std::get<3>(t)), // t4[0]
std::get<1>(std::get<3>(t)), // t4[1]

t2 是空的,所以我们不从中提取任何东西。)

第一列整数是 outer 数组,第二列是 inner 数组,这些是我们需要的计算结果。但首先,让我们处理 tuple_cat 的返回类型。

tuple_cat 的返回类型就是参数的连接,被视为类型列表。连接列表的元编程算法在 Eric Niebler 的 Meta 库中被称为 meta::concat,但我将称它为 mp_append,以其经典的 Lisp 名称命名。

(Lisp 就像今天的拉丁语。受过教育的人应该学习过然后忘记它。)

template<class... L> struct mp_append_impl;

template<class... L> using mp_append = typename mp_append_impl<L...>::type;

template<> struct mp_append_impl<>
{
    using type = mp_list<>;
};

template<template<class...> class L, class... T> struct mp_append_impl<L<T...>>
{
    using type = L<T...>;
};

template<template<class...> class L1, class... T1,
    template<class...> class L2, class... T2, class... Lr>
    struct mp_append_impl<L1<T1...>, L2<T2...>, Lr...>
{
    using type = mp_append<L1<T1..., T2...>, Lr...>;
};

这相当容易。还有其他实现 mp_append 的方法,但这种方法展示了语言如何通过包展开为我们完成了大部分工作。这是 C++11 中的一个常见主题。

请注意 mp_append 返回的列表类型与其第一个参数相同。当然,在没有给出参数的情况下,没有第一个参数可以从中获取类型,所以我随意选择返回一个空的 mp_list

我们现在准备好了 tuple_cat 的声明

template<class... Tp,
    class R = mp_append<typename std::remove_reference<Tp>::type...>>
    R tuple_cat( Tp &&... tp );

我们需要 remove_reference 的原因是右值引用参数,用于实现完美转发。如果参数是左值,例如上面的 t1,其对应的类型将是元组的引用——在这种情况下,t1 的类型是 std::tuple<int, short, long>&。我们的原语不识别元组的引用作为类型列表,因此我们需要剥离它们。

但是我们的返回类型计算有两个问题。一,如果 tuple_cat 不带任何参数调用怎么办?在这种情况下,我们返回 mp_list<>,但正确的结果是 std::tuple<>

二,如果我们用第一个参数是 std::pairtuple_cat 调用怎么办?我们将尝试将更多元素附加到 std::pair,这将失败。

通过将一个空的元组作为 mp_append 的第一个参数,我们可以解决这两个问题

template<class... Tp,
    class R = mp_append<std::tuple<>, typename std::remove_reference<Tp>::type...>>
    R tuple_cat( Tp &&... tp );

处理好返回类型后,让我们继续计算 inner。我们有

[x1, x2, x3], [], [y1, y2, y3], [z1, z2]

作为输入,我们需要输出

[0, 0, 0, 2, 2, 2, 3, 3]

它是以下内容的连接

[0, 0, 0], [], [2, 2, 2], [3, 3]

这里每个元组的大小都与输入相同,但填充了代表其在参数列表中索引的常量。第一个元组填充 0,第二个填充 1,第三个填充 2,依此类推。

如果我们首先计算一个索引列表(在本例中是 [0, 1, 2, 3]),然后对两个列表使用二元 mp_transform,我们可以达到这个结果

[[x1, x2, x3], [], [y1, y2, y3], [z1, z2]]
[0, 1, 2, 3]

以及一个函数,它接受一个列表和一个整数(以 std::integral_constant 的形式)并返回一个与原始列表大小相同的列表,但填充了第二个参数。

我们将这个函数称为 mp_fill,模仿 std::fill

函数式程序员会立即意识到 mp_fillmp_transform,它带有一个返回常量的函数,这里是沿着这条线的一个实现

template<class V> struct mp_constant
{
    template<class...> using apply = V;
};

template<class L, class V>
    using mp_fill = mp_transform<mp_constant<V>::template apply, L>;

这里是另一个实现

template<class L, class V> struct mp_fill_impl;

template<template<class...> class L, class... T, class V>
    struct mp_fill_impl<L<T...>, V>
{
    template<class...> using _fv = V;
    using type = L<_fv<T>...>;
};

template<class L, class V> using mp_fill = typename mp_fill_impl<L, V>::type;

这些展示了不同的风格,选择哪一种很大程度上取决于口味。在第一种情况下,我们组合了现有的原语;在第二种情况下,我们 "内联" 了 mp_const 甚至 mp_transformmp_fill_impl 的主体中。

大多数 C++11 程序员可能会觉得第二个实现更容易阅读。

现在我们可以 mp_fill 了,但我们仍然需要 [0, 1, 2, 3] 索引序列。我们可以为此编写一个算法 mp_iota(模仿 std::iota),但 C++14 已经有一个生成索引序列的标准方法,称为 std::make_index_sequence。由于 Eric 的原始解决方案使用了 make_index_sequence,让我们也照此办理。

严格来说,这让我们超出了 C++11 的范围,但 make_index_sequence 并不难实现(如果效率不是重点的话)

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

template<class S> struct next_integer_sequence;

template<class T, T... Ints> struct next_integer_sequence<integer_sequence<T, Ints...>>
{
    using type = integer_sequence<T, Ints..., sizeof...(Ints)>;
};

template<class T, T I, T N> struct make_int_seq_impl;

template<class T, T N>
    using make_integer_sequence = typename make_int_seq_impl<T, 0, N>::type;

template<class T, T I, T N> struct make_int_seq_impl
{
    using type = typename next_integer_sequence<
        typename make_int_seq_impl<T, I+1, N>::type>::type;
};

template<class T, T N> struct make_int_seq_impl<T, N, N>
{
    using type = integer_sequence<T>;
};

template<std::size_t... Ints>
    using index_sequence = integer_sequence<std::size_t, Ints...>;

template<std::size_t N>
    using make_index_sequence = make_integer_sequence<std::size_t, N>;

我们现在可以得到一个 index_sequence<0, 1, 2, 3>

template<class... Tp,
    class R = mp_append<std::tuple<>, typename std::remove_reference<Tp>::type...>>
    R tuple_cat( Tp &&... tp )
{
    std::size_t const N = sizeof...(Tp);

    // inner

    using seq = make_index_sequence<N>;
}

make_index_sequence<4> 返回 integer_sequence<std::size_t, 0, 1, 2, 3>,它不是一个类型列表。为了处理它,我们需要将其转换为类型列表,因此我们将引入一个名为 mp_from_sequence 的函数来完成此操作。

template<class S> struct mp_from_sequence_impl;

template<template<class T, T... I> class S, class U, U... J>
    struct mp_from_sequence_impl<S<U, J...>>
{
    using type = mp_list<std::integral_constant<U, J>...>;
};

template<class S> using mp_from_sequence = typename mp_from_sequence_impl<S>::type;

我们现在可以计算我们想要用 mp_fill 转换的两个列表

template<class... Tp,
    class R = mp_append<std::tuple<>, typename std::remove_reference<Tp>::type...>>
    R tuple_cat( Tp &&... tp )
{
    std::size_t const N = sizeof...(Tp);

    // inner

    using list1 = mp_list<typename std::remove_reference<Tp>::type...>;
    using list2 = mp_from_sequence<make_index_sequence<N>>;

    // list1: [[x1, x2, x3], [], [y1, y2, y3], [z1, z2]]
    // list2: [0, 1, 2, 3]

    return R{};
}

并完成计算 inner 的工作

template<class... Tp,
    class R = mp_append<std::tuple<>, typename std::remove_reference<Tp>::type...>>
    R tuple_cat( Tp &&... tp )
{
    std::size_t const N = sizeof...(Tp);

    // inner

    using list1 = mp_list<typename std::remove_reference<Tp>::type...>;
    using list2 = mp_from_sequence<make_index_sequence<N>>;

    // list1: [[x1, x2, x3], [], [y1, y2, y3], [z1, z2]]
    // list2: [0, 1, 2, 3]

    using list3 = mp_transform<mp_fill, list1, list2>;

    // list3: [[0, 0, 0], [], [2, 2, 2], [3, 3]]

    using inner = mp_rename<list3, mp_append>; // or mp_apply<mp_append, list3>

    // inner: [0, 0, 0, 2, 2, 2, 3, 3]

    return R{};
}

对于 outer,我们再次有

[x1, x2, x3], [], [y1, y2, y3], [z1, z2]

作为输入,我们需要输出

[0, 1, 2, 0, 1, 2, 0, 1]

它是以下内容的连接

[0, 1, 2], [], [0, 1, 2], [0, 1]

这里的区别是,我们不是用一个常量来填充元组,而是需要用从 0 开始递增的值来填充它,即使用 make_index_sequence<N> 的结果,其中 N 是元素的数量。

最直接的方法是定义一个我们想要的元函数 F,然后使用 mp_transform 将其应用于输入

template<class N> using mp_iota = mp_from_sequence<make_index_sequence<N::value>>;

template<class L> using F = mp_iota<mp_size<L>>;

template<class... Tp,
    class R = mp_append<std::tuple<>, typename std::remove_reference<Tp>::type...>>
    R tuple_cat( Tp &&... tp )
{
    std::size_t const N = sizeof...(Tp);

    // outer

    using list1 = mp_list<typename std::remove_reference<Tp>::type...>;
    using list2 = mp_transform<F, list1>;

    // list2: [[0, 1, 2], [], [0, 1, 2], [0, 1]]

    using outer = mp_rename<list2, mp_append>;

    // outer: [0, 1, 2, 0, 1, 2, 0, 1]

    return R{};
}

嗯,这很容易。出奇地容易。唯一的小麻烦是我们不能在 tuple_cat 内部定义 F——模板不能在函数中定义。

让我们把所有东西都放在一起。

template<class N> using mp_iota = mp_from_sequence<make_index_sequence<N::value>>;

template<class L> using F = mp_iota<mp_size<L>>;

template<class R, class...Is, class... Ks, class Tp>
R tuple_cat_( mp_list<Is...>, mp_list<Ks...>, Tp tp )
{
    return R{ std::get<Ks::value>(std::get<Is::value>(tp))... };
}

template<class... Tp,
    class R = mp_append<std::tuple<>, typename std::remove_reference<Tp>::type...>>
    R tuple_cat( Tp &&... tp )
{
    std::size_t const N = sizeof...(Tp);

    // inner

    using list1 = mp_list<typename std::remove_reference<Tp>::type...>;
    using list2 = mp_from_sequence<make_index_sequence<N>>;

    // list1: [[x1, x2, x3], [], [y1, y2, y3], [z1, z2]]
    // list2: [0, 1, 2, 3]

    using list3 = mp_transform<mp_fill, list1, list2>;

    // list3: [[0, 0, 0], [], [2, 2, 2], [3, 3]]

    using inner = mp_rename<list3, mp_append>; // or mp_apply<mp_append, list3>

    // inner: [0, 0, 0, 2, 2, 2, 3, 3]

    // outer

    using list4 = mp_transform<F, list1>;

    // list4: [[0, 1, 2], [], [0, 1, 2], [0, 1]]

    using outer = mp_rename<list4, mp_append>;

    // outer: [0, 1, 2, 0, 1, 2, 0, 1]

    return tuple_cat_<R>( inner(), outer(),
        std::forward_as_tuple( std::forward<Tp>(tp)... ) );
}

这几乎可以编译,除了我们的 inner 碰巧是一个 std::tuple,而我们的辅助函数期望的是一个 mp_list。(outer 碰巧已经是 mp_list 了。)我们可以轻松修复它。

return tuple_cat_<R>( mp_rename<inner, mp_list>(), outer(),
    std::forward_as_tuple( std::forward<Tp>(tp)... ) );

让我们定义一个 print_tuple 函数,看看一切是否都正常。

template<int I, int N, class... T> struct print_tuple_
{
    void operator()( std::tuple<T...> const & tp ) const
    {
        using Tp = typename std::tuple_element<I, std::tuple<T...>>::type;

        print_type<Tp>( " ", ": " );

        std::cout << std::get<I>( tp ) << ";";

        print_tuple_< I+1, N, T... >()( tp );
    }
};

template<int N, class... T> struct print_tuple_<N, N, T...>
{
    void operator()( std::tuple<T...> const & ) const
    {
    }
};

template<class... T> void print_tuple( std::tuple<T...> const & tp )
{
    std::cout << "{";
    print_tuple_<0, sizeof...(T), T...>()( tp );
    std::cout << " }\n";
}

int main()
{
    std::tuple<int, long> t1{ 1, 2 };
    std::tuple<> t2;
    std::tuple<float, double, long double> t3{ 3, 4, 5 };
    std::pair<void const*, char const*> t4{ "pv", "test" };

    using expected = std::tuple<int, long, float, double, long double,
        void const*, char const*>;

    auto result = ::tuple_cat( t1, t2, t3, t4 );

    static_assert( std::is_same<decltype(result), expected>::value, "" );

    print_tuple( result );
}

输出:

{ int: 1; long: 2; float: 3; double: 4; long double: 5; void const*: 0x407086;
    char const*: test; }

看起来可行。但至少还有一个错误。要了解原因,请替换第一个元组

std::tuple<int, long> t1{ 1, 2 };

用一对

std::pair<int, long> t1{ 1, 2 };

我们现在在以下位置收到一个错误

using inner = mp_rename<list3, mp_append>;

因为 list3 的第一个元素是 std::pairmp_append 尝试将其用作返回类型,但失败了。

有两种方法可以解决这个问题。第一种方法是使用我们为返回类型设计的相同技巧,并将一个空的 mp_list 插入到 list3 的前面,mp_append 将使用它作为返回类型

using inner = mp_rename<mp_push_front<list3, mp_list<>>, mp_append>;

第二种方法是简单地将所有输入转换为 mp_list

using list1 = mp_list<
    mp_rename<typename std::remove_reference<Tp>::type, mp_list>...>;

在两种情况下,inner 现在都将是一个 mp_list,因此我们可以省略调用 tuple_cat_ 时的 mp_rename

我们完成了。结果应该不言而喻。

高阶元编程,或其缺失

也许到现在你可能在想,为什么这篇文章叫做“简单的 C++11 元编程”,因为我们到目前为止所讲的并不特别简单。

我们方法的相对简单性源于我们没有进行任何高阶元编程,也就是说,我们没有引入任何返回元函数的原语,例如 composebind 或 lambda 库。

我认为,在大多数情况下,这种高阶元编程在 C++11 中是不必要的。例如,考虑上面给出的 Eric Niebler 的解决方案

using outer =
    typelist_cat_t<
        typelist_transform_t<
            typelist<as_typelist_t<Tuples>...>,
            meta_compose<
                meta_quote<as_typelist_t>,
                meta_quote_i<std::size_t, make_index_sequence>,
                meta_quote<typelist_size_t> > > >;

meta_compose 表达式接受三个其他("引用的")元函数,并创建一个新的元函数,按顺序应用它们。Eric 用这个例子作为动机来引入 "元函数类" 的概念,然后提供各种操作元函数类的原语。

但是当我们有元函数 FGH 时,而不是使用 meta_compose,在 C++11 中,我们可以简单地这样做

template<class... T> using Fgh = F<G<H<T...>>>;

就这样。语言使得定义复合函数变得容易,并且不需要库支持。如果需要组合的函数是 as_typelist_tstd::make_index_sequencetypelist_size_t,我们只需定义

template<class... T>
    using F = as_typelist_t<std::make_index_sequence<typelist_size_t<T...>::value>>;

类似地,如果我们需要的元函数会返回 sizeof(T) < sizeof(U),则无需像这样 enlist 元编程 lambda 库

lambda<_a, _b, less<sizeof_<_a>, sizeof_<_b>>>>

我们可以直接内联定义它

template<class T, class U> using sizeof_less = mp_bool<(sizeof(T) < sizeof(U))>;

再说一件事

最后,我将展示 mp_countmp_count_if 的实现,仅仅是因为我觉得它们很有趣。mp_count<L, V> 返回类型 V 在列表 L 中的出现次数;mp_count_if<L, P> 计算 LP<T>true 的类型数量。

作为第一步,我将实现 mp_plusmp_plus 是一个可变的(不仅仅是二元的)元函数,它返回其参数的总和。

template<class... T> struct mp_plus_impl;

template<class... T> using mp_plus = typename mp_plus_impl<T...>::type;

template<> struct mp_plus_impl<>
{
    using type = std::integral_constant<int, 0>;
};

template<class T1, class... T> struct mp_plus_impl<T1, T...>
{
    static constexpr auto _v = T1::value + mp_plus<T...>::value;

    using type = std::integral_constant<
        typename std::remove_const<decltype(_v)>::type, _v>;
};

现在我们有了 mp_plusmp_count 只是

template<class L, class V> struct mp_count_impl;

template<template<class...> class L, class... T, class V>
    struct mp_count_impl<L<T...>, V>
{
    using type = mp_plus<std::is_same<T, V>...>;
};

template<class L, class V> using mp_count = typename mp_count_impl<L, V>::type;

这是参数包展开的强大功能的又一个例子。可惜我们不能在 mp_plus 中也使用包展开来获得

T1::value + T2::value + T3::value + T4::value + ...

直接。如果支持 T::value + ... 会很好,并且看起来在 C++17 中,它将支持。

mp_count_if 同样直接

template<class L, template<class...> class P> struct mp_count_if_impl;

template<template<class...> class L, class... T, template<class...> class P>
    struct mp_count_if_impl<L<T...>, P>
{
    using type = mp_plus<P<T>...>;
};

template<class L, template<class...> class P>
    using mp_count_if = typename mp_count_if_impl<L, P>::type;

至少如果我们要求 P 返回 bool。否则,我们将不得不将 P<T>::value 强制转换为 0 或 1,否则计数将不正确。

template<bool v> using mp_bool = std::integral_constant<bool, v>;

template<class L, template<class...> class P> struct mp_count_if_impl;

template<template<class...> class L, class... T, template<class...> class P>
    struct mp_count_if_impl<L<T...>, P>
{
    using type = mp_plus<mp_bool<P<T>::value != 0>...>;
};

template<class L, template<class...> class P>
    using mp_count_if = typename mp_count_if_impl<L, P>::type;

我将展示的最后一个原语是 mp_containsmp_contains<L, V> 返回列表 L 是否包含类型 V

template<class L, class V> using mp_contains = mp_bool<mp_count<L, V>::value != 0>;

乍一看,这个实现似乎非常天真且效率低下——如果我们只对布尔结果感兴趣,为什么需要计算所有出现次数然后将其丢弃——但实际上它相当有竞争力且完全可用。我们只需要对 mp_plusmp_countmp_contains 的引擎)进行一点优化

template<class T1, class T2, class T3, class T4, class T5,
    class T6, class T7, class T8, class T9, class T10, class... T>
    struct mp_plus_impl<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T...>
{
    static constexpr auto _v = T1::value + T2::value + T3::value + T4::value +
        T5::value + T6::value + T7::value + T8::value + T9::value + T10::value +
        mp_plus<T...>::value;

    using type = std::integral_constant<
        typename std::remove_const<decltype(_v)>::type, _v>;
};

这大约将模板实例化次数减少了十倍。

结论

我概述了一种 C++11 元编程的方法,该方法

  • 利用了可变模板、参数包展开和模板别名;

  • 操作任何可变模板 L<T...>,将其作为基本数据结构,而不强制要求特定的类型列表表示;

  • 使用模板别名作为元函数,表达式 F<T...> 作为函数调用的等价物;

  • 利用数据结构 L<T...> 和元函数调用 F<T...> 之间的结构相似性;

  • 尽可能利用参数包展开,而不是使用传统的递归实现;

  • 依靠模板别名的内联定义来实现函数组合,而不是为此任务提供库支持。

进一步阅读

第二部分现已发布,其中我展示了允许我们将类型列表视为集合、映射和向量的算法,并在过程中演示了各种 C++11 实现技术。