Boost C++ 库

...世界上最受推崇和设计最精良的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, 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;

是的,我们完全可以,这种改进使我们可以在任何其他类型列表(例如 std::tuple)上使用 mp_size。它将 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_length 来实现 mp_size?一种选择是将后者的实现直接替换到前者中

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。我们将使用 transform,以与已建立的 C++ 实践保持一致(map 在 STL 和 Boost.MPL 中都是数据结构。)

我们将把我们的算法称为 mp_transformmp_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,但我将根据其经典的 Lisp 名称将其称为 mp_append

(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::pair 作为第一个参数调用 tuple_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 的形式),并返回一个与原始列表大小相同但填充了第二个参数的列表。

我们将根据 std::fill 将此函数称为 mp_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_fill_impl 的主体中“内联”了 mp_const 甚至 mp_transform

大多数 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 尝试并失败将其用作其返回类型。

有两种方法可以解决这个问题。第一种方法是应用我们用于返回类型的相同技巧,并在 list3 的前面插入一个空的 mp_listmp_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 时,在 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) 的元函数,则无需像下面这样使用元编程 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...> 之间的结构相似性;

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

  • 依赖于模板别名的内联定义进行函数组合,而不是为此任务提供库支持。

进一步阅读

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