借助可变模板、参数包和模板别名
|
注意
|
我写这篇文章的灵感来自于我阅读了 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)和关键字(如 if、int 或 true)之间的名称冲突将很常见,并且会成为问题的根源。使用前缀,我们就可以避免所有这些麻烦。
所以我们有了类型列表,并可以往里面放东西
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_size 和 mp_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_size 以 mp_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_front。mp_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 所解释的,这个实现依赖于一个巧妙的技巧:将输入元组打包成一个元组,创建两个索引数组 inner 和 outer,然后用外部索引和结果(其中一个输入元组)来索引外部元组,再用内部索引来索引它。
因此,例如,如果 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::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 的形式)并返回一个与原始列表大小相同的列表,但填充了第二个参数。
我们将这个函数称为 mp_fill,模仿 std::fill。
函数式程序员会立即意识到 mp_fill 是 mp_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_transform 在 mp_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::pair,mp_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 元编程”,因为我们到目前为止所讲的并不特别简单。
我们方法的相对简单性源于我们没有进行任何高阶元编程,也就是说,我们没有引入任何返回元函数的原语,例如 compose、bind 或 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 用这个例子作为动机来引入 "元函数类" 的概念,然后提供各种操作元函数类的原语。
但是当我们有元函数 F、G 和 H 时,而不是使用 meta_compose,在 C++11 中,我们可以简单地这样做
template<class... T> using Fgh = F<G<H<T...>>>;
就这样。语言使得定义复合函数变得容易,并且不需要库支持。如果需要组合的函数是 as_typelist_t、std::make_index_sequence 和 typelist_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_count 和 mp_count_if 的实现,仅仅是因为我觉得它们很有趣。mp_count<L, V> 返回类型 V 在列表 L 中的出现次数;mp_count_if<L, P> 计算 L 中 P<T> 为 true 的类型数量。
作为第一步,我将实现 mp_plus。mp_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_plus,mp_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_contains。mp_contains<L, V> 返回列表 L 是否包含类型 V
template<class L, class V> using mp_contains = mp_bool<mp_count<L, V>::value != 0>;
乍一看,这个实现似乎非常天真且效率低下——如果我们只对布尔结果感兴趣,为什么需要计算所有出现次数然后将其丢弃——但实际上它相当有竞争力且完全可用。我们只需要对 mp_plus(mp_count 和 mp_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 实现技术。