使用可变模板、参数包和模板别名
注意
|
在我读完 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_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
。为了与已建立的 C++ 实践保持一致,我们将使用 transform
(map
是 STL 和 Boost.MPL 中的数据结构)。
我们将把我们的算法称为 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_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::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...>
之间的结构相似性; -
尽可能利用参数包展开,而不是使用传统的递归实现;
-
依赖模板别名的内联定义进行函数组合,而不是为此任务提供库支持。
进一步阅读
第 2 部分现已推出,我将在其中展示允许我们将类型列表视为集合、映射和向量的算法,并在此过程中演示各种 C++11 实现技术。