向量、集合和映射
在上篇文章中,我概述了一种对类型列表进行操作的元编程风格——可变参数类模板。
template<class... T> struct mp_list {};
经典的 Lisp 将列表作为其唯一的数据结构,但操作列表通常与其元素数量成线性关系。
除了 list
,STL 还有 vector
、set
和 map
。vector
支持按索引进行随机访问;set
支持高效的成员测试;map
将键与值关联,并支持基于键的高效查找。
我们不引入 mp_vector
、mp_set
、mp_map
这样的独立数据结构,而是将数据保持为列表形式,并尝试提供高效的随机访问、成员测试和查找算法。
mp_contains
我们从集合开始。集合就是元素唯一的列表。为了从任意列表中获得一个集合,我们需要一个去除重复项的算法。我们称之为 mp_unique<L>
。
// mp_if
template<bool C, class T, class E> struct mp_if_c_impl;
template<class T, class E> struct mp_if_c_impl<true, T, E>
{
using type = T;
};
template<class T, class E> struct mp_if_c_impl<false, T, E>
{
using type = E;
};
template<bool C, class T, class E>
using mp_if_c = typename mp_if_c_impl<C, T, E>::type;
template<class C, class T, class E>
using mp_if = typename mp_if_c_impl<C::value != 0, T, E>::type;
// mp_unique
template<class L> struct mp_unique_impl;
template<class L> using mp_unique = typename mp_unique_impl<L>::type;
template<template<class...> class L> struct mp_unique_impl<L<>>
{
using type = L<>;
};
template<template<class...> class L, class T1, class... T>
struct mp_unique_impl<L<T1, T...>>
{
using _rest = mp_unique<L<T...>>;
using type = mp_if<mp_contains<_rest, T1>, _rest, mp_push_front<_rest, T1>>;
};
对于成员测试,我们已经介绍了一个算法 mp_contains<L, V>
,当 L
包含 V
时返回 true
。mp_contains
的直接递归实现如下:
template<class L, class V> struct mp_contains_impl;
template<class L, class V> using mp_contains = typename mp_contains_impl<L, V>::type;
template<template<class...> class L, class V> struct mp_contains_impl<L<>, V>
{
using type = std::false_type;
};
template<template<class...> class L, class... T, class V>
struct mp_contains_impl<L<V, T...>, V>
{
using type = std::true_type;
};
template<template<class...> class L, class T1, class... T, class V>
struct mp_contains_impl<L<T1, T...>, V>: mp_contains_impl<L<T...>, V>
{
};
请注意,mp_unique<L>
会对 mp_contains
调用 N 次,其中 N 是列表 L
的长度。这意味着 mp_contains
需要尽可能快,而上面的实现,嗯,并不快。
以下是调用 mp_unique
处理包含 N 个(不同)元素的列表的编译时间(秒):
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,递归 |
2.1 |
DNF |
||||||
clang++ 3.5.1,递归 |
0.9 |
4.5 |
13.2 |
30.2 |
DNF |
|||
g++ 4.9.2,递归 |
0.7 |
3.6 |
10.4 |
23.2 |
DNF |
(测试在 Windows/Cygwin 下进行。所有编译器均为 32 位。无优化。DNF 表示“未完成”,通常意味着编译器耗尽了堆空间或崩溃了。)
显然我们需要更好的替代方案。
我在上一篇文章的结尾实现了一个依赖于 mp_count
的 mp_contains
,而 mp_count
又依赖于 mp_plus
。让我们看看它的表现如何:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,mp_count/mp_plus |
1.1 |
9.8 |
50.5 |
DNF |
||||
clang++ 3.5.1,mp_count/mp_plus |
0.5 |
1.4 |
3.1 |
6.1 |
DNF |
|||
g++ 4.9.2,mp_count/mp_plus |
0.5 |
1.3 |
2.9 |
5.8 |
9.7 |
15.6 |
22.4 |
32.3 |
至少如果你的编译器碰巧是 g++
,情况还不算太糟。尽管如此,这里仍然有改进的空间。
为了做得更好,我们必须以某种方式利用语言特性,例如包展开(pack expansion),让它们为我们做更多的工作。为了获得灵感,我们参考 C++11 标准第 14.5.3 段第 4 条,该条款解释了包展开可以在以下上下文中发生:
-
在函数参数包(8.3.5)中;模式是省略号之前的参数声明。
-
在模板参数包中,它是一个包展开(14.1)。
-
在初始化列表(8.5)中;模式是初始化子句。
-
在基类说明符列表(第 10 章)中;模式是基类说明符。
-
在成员初始化列表(12.6.2)中;模式是成员初始化符。
-
在模板参数列表(14.3)中;模式是模板参数。
-
在动态异常说明(15.4)中;模式是类型标识。
-
在属性列表(7.6.1)中;模式是属性。
-
在对齐说明符(7.6.2)中;模式是省略号之前的对齐说明符。
-
在捕获列表(5.1.2)中;模式是捕获。
-
在
sizeof...
表达式(5.3.3)中;模式是标识符。
**加粗**是我添加的,用于指示可能的线索。
我们的第一个选择是将参数包展开为函数调用的参数。由于我们对在编译时发生的运算感兴趣,调用函数可能看起来没用;但 C++11 函数可以是 constexpr
,而 constexpr
函数的“调用”确实发生在编译时。
回想一下我们的 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;
我们可以使用 constexpr
函数来代替模板别名 mp_plus
对 is_same
表达式进行求和:
constexpr std::size_t cx_plus()
{
return 0;
}
template<class T1, class... T> constexpr std::size_t cx_plus(T1 t1, T... t)
{
return t1 + cx_plus(t...);
}
// mp_size_t
template<std::size_t N> using mp_size_t = std::integral_constant<std::size_t, N>;
// 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_size_t<cx_plus(std::is_same<T, V>::value...)>;
};
template<class L, class V> using mp_count = typename mp_count_impl<L, V>::type;
结果如下:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
clang++ 3.5.1,mp_count/cx_plus |
0.4 |
1.1 |
2.5 |
5.0 |
DNF |
|||
g++ 4.9.2,mp_count/cx_plus |
0.4 |
0.9 |
1.7 |
2.9 |
4.7 |
6.7 |
9.2 |
11.8 |
我们改进了时间,但失去了 VC++ 2013,因为它没有实现 constexpr
。
让我们尝试将包展开为初始化列表。与其将 is_same
表达式传递给函数,不如用它们构建一个常量数组,然后用 constexpr
函数对数组进行求和:
constexpr std::size_t cx_plus2(bool const * first, bool const * last)
{
return first == last? 0: *first + cx_plus2(first + 1, last);
}
// 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>
{
static constexpr bool _v[] = { std::is_same<T, V>::value... };
using type = mp_size_t<cx_plus2(_v, _v + sizeof...(T))>;
};
template<class L, class V> using mp_count = typename mp_count_impl<L, V>::type;
这是一个巧妙的技巧,但它快吗?
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
clang++ 3.5.1,mp_count/cx_plus2 |
0.4 |
0.9 |
1.8 |
DNF |
||||
g++ 4.9.2,mp_count/cx_plus2 |
0.4 |
0.9 |
1.9 |
3.4 |
5.4 |
7.8 |
11.0 |
14.7 |
这有点令人失望。让我们看看如何通过将参数包展开到基类说明符列表(base-specifier-list)中来解决这个问题。我们可以定义一个派生自包中每个元素的类:
struct U: T... {};
然后我们可以使用 std::is_base_of<V, U>
来测试类型 V
是否是 U
的基类,即它是否是参数包中的一个元素。这正是我们所需要的。
void
、int
或 void(int)
等任意类型不能用作基类,但我们可以将它们包装在一个空的类模板中,我们称之为 mp_identity
。
template<class T> struct mp_identity
{
using type = T;
};
template<class L, class V> struct mp_contains_impl;
template<class L, class V> using mp_contains = typename mp_contains_impl<L, V>::type;
template<template<class...> class L, class... T, class V>
struct mp_contains_impl<L<T...>, V>
{
struct U: mp_identity<T>... {};
using type = std::is_base_of<mp_identity<V>, U>;
};
性能如何?
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,is_base_of |
0.3 |
0.6 |
1.3 |
2.5 |
DNF |
|||
clang++ 3.5.1,is_base_of |
0.3 |
0.4 |
0.6 |
0.8 |
DNF |
|||
g++ 4.9.2,is_base_of |
0.3 |
0.4 |
0.6 |
0.9 |
1.3 |
1.7 |
2.3 |
3.0 |
这个实现明显是赢家。
公平地说,我们应该注意到,前四种 mp_contains
的实现不依赖于列表元素的唯一性。这使得 mp_contains
成为一个支持任意列表的算法,而不仅仅是集合。
然而,is_base_of
实现不支持包含重复项的列表,因为不能直接从同一类型继承两次。因此,它没有实现通用的 mp_contains
,而是实现了一个可能应该命名为 mp_set_contains
的东西。
我们可以通过修改实现,使其通过中间基类间接继承自 mp_identity<T>
来避免“不允许重复”的要求:
// indirect_inherit
template<std::size_t I, class T> struct inherit_second: T {};
template<class L, class S> struct indirect_inherit_impl;
template<template<class...> class L, class... T, std::size_t... J>
struct indirect_inherit_impl<L<T...>, integer_sequence<std::size_t, J...>>:
inherit_second<J, mp_identity<T>>... {};
template<class L> using indirect_inherit =
indirect_inherit_impl<L, make_index_sequence<mp_size<L>::value>>;
// mp_contains
template<class L, class V> struct mp_contains_impl
{
using U = indirect_inherit<L>;
using type = std::is_base_of<mp_identity<V>, U>;
};
template<class L, class V> using mp_contains = typename mp_contains_impl<L, V>::type;
然而,这几乎抵消了我们通过原始 is_base_of
实现观察到的惊人的性能提升:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,递归 |
2.1 |
DNF |
||||||
VC++ 2013,mp_count/mp_plus |
1.1 |
9.8 |
50.5 |
DNF |
||||
VC++ 2013,is_base_of |
0.3 |
0.6 |
1.3 |
2.5 |
DNF |
|||
VC++ 2013,is_base_of/indirect |
1.0 |
9.3 |
49.5 |
153.8 |
DNF |
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
clang++ 3.5.1,递归 |
0.9 |
4.5 |
13.2 |
30.2 |
DNF |
|||
clang++ 3.5.1,mp_count/mp_plus |
0.5 |
1.4 |
3.1 |
6.1 |
DNF |
|||
clang++ 3.5.1,mp_count/cx_plus |
0.4 |
1.1 |
2.5 |
5.0 |
DNF |
|||
clang++ 3.5.1,mp_count/cx_plus2 |
0.4 |
0.9 |
1.8 |
DNF |
||||
clang++ 3.5.1,is_base_of |
0.3 |
0.4 |
0.6 |
0.8 |
DNF |
|||
clang++ 3.5.1,is_base_of/indirect |
0.4 |
0.9 |
1.6 |
2.5 |
DNF |
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
g++ 4.9.2,递归 |
0.7 |
3.6 |
10.4 |
23.2 |
DNF |
|||
g++ 4.9.2,mp_count/mp_plus |
0.5 |
1.3 |
2.9 |
5.8 |
9.7 |
15.6 |
22.4 |
32.3 |
g++ 4.9.2,mp_count/cx_plus |
0.4 |
0.9 |
1.7 |
2.9 |
4.7 |
6.7 |
9.2 |
11.8 |
g++ 4.9.2,mp_count/cx_plus2 |
0.4 |
0.9 |
1.9 |
3.4 |
5.4 |
7.8 |
11.0 |
14.7 |
g++ 4.9.2,is_base_of |
0.3 |
0.4 |
0.6 |
0.9 |
1.3 |
1.7 |
2.3 |
3.0 |
g++ 4.9.2,is_base_of/indirect |
0.5 |
1.1 |
2.3 |
4.0 |
6.6 |
9.8 |
13.6 |
18.2 |
mp_map_find
在 STL 的意义上,映射(map)是一种将键与值关联起来的数据结构,并且可以高效地根据键检索其关联值。对我们而言,映射将是任何列表的列表,其中内部列表至少有一个元素(键);其余元素将被视为关联值。例如,列表:
[[A, B], [C, D, E], [F], [G, H]]
是一个映射,其键为 A
、C
、F
和 G
,关联值为 [B]
、[D, E]
、[]
和 [H]
。出于稍后会显而易见的原因,我们将要求键是唯一的。
我将展示另外两个映射的例子,这次使用真实的 C++ 代码:
using Map = mp_list<mp_list<int, int*>, mp_list<void, void*>, mp_list<char, char*>>;
using Map2 = std::tuple<std::pair<int, int[2]>, std::pair<char, char[2]>>;
执行基于键的检索的算法的 Lisp 名称是 ASSOC
,但我将其称为 mp_map_find
。mp_map_find<M, K>
返回 M
中第一个元素为 K
的元素。例如,mp_map_find<Map2, int>
将返回 std::pair<int, int[2]>
。如果不存在这样的键,则返回 void
。
几乎没有必要实现和基准测试 mp_map_find
的递归版本——我们可以相当肯定它将表现糟糕。尽管如此:
template<class M, class K> struct mp_map_find_impl;
template<class M, class K> using mp_map_find = typename mp_map_find_impl<M, K>::type;
template<template<class...> class M, class K> struct mp_map_find_impl<M<>, K>
{
using type = void;
};
template<template<class...> class M, class T1, class... T, class K>
struct mp_map_find_impl<M<T1, T...>, K>
{
using type = mp_if<std::is_same<mp_front<T1>, K>, T1, mp_map_find<M<T...>, K>>;
};
N
次查找一个大小为 N
的映射的编译时间(秒)如下:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,递归 |
38.2 |
DNF |
||||||
clang++ 3.5.1,递归 |
2.5 |
13.7 |
DNF |
|||||
g++ 4.9.2,递归 |
1.9 |
10.2 |
28.8 |
DNF |
我告诉过你没有意义。
但是,我听到有人说,即使条件为真,你也在评估 else 分支,这效率非常低下!
嗯,这最多只会将性能提高大约两倍,而且仅当元素存在时才有效,但好吧,我们试试。基准测试中元素恰好存在,所以我们来看看。
// mp_eval_if
template<bool C, class T, template<class...> class E, class... A>
struct mp_eval_if_c_impl;
template<class T, template<class...> class E, class... A>
struct mp_eval_if_c_impl<true, T, E, A...>
{
using type = T;
};
template<class T, template<class...> class E, class... A>
struct mp_eval_if_c_impl<false, T, E, A...>
{
using type = E<A...>;
};
template<bool C, class T, template<class...> class E, class... A>
using mp_eval_if_c = typename mp_eval_if_c_impl<C, T, E, A...>::type;
template<class C, class T, template<class...> class E, class... A>
using mp_eval_if = typename mp_eval_if_c_impl<C::value != 0, T, E, A...>::type;
// mp_map_find
template<class M, class K> struct mp_map_find_impl;
template<class M, class K> using mp_map_find = typename mp_map_find_impl<M, K>::type;
template<template<class...> class M, class K> struct mp_map_find_impl<M<>, K>
{
using type = void;
};
template<template<class...> class M, class T1, class... T, class K>
struct mp_map_find_impl<M<T1, T...>, K>
{
using type = mp_eval_if<std::is_same<mp_front<T1>, K>, T1, mp_map_find, M<T...>, K>;
};
看吧:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,递归 |
15.6 |
DNF |
||||||
clang++ 3.5.1,递归 |
1.8 |
9.5 |
DNF |
|||||
g++ 4.9.2,递归 |
1.4 |
7.0 |
19.7 |
DNF |
我告诉过你没有意义。
不论如何,确定递归实现效率低下与提出一个高效的实现是两回事。有两点使得 mp_contains
的技术不适用于我们当前的情况:首先,mp_contains
只需返回 true 或 false,而 mp_map_find
返回一个类型;其次,在 mp_contains
中我们知道我们正在查找的元素的精确类型,而在本例中,我们只知道它的 mp_front
。
幸运的是,确实存在一个语言特性可以解决这两个问题:C++ 可以通过派生类来推导基类的模板参数。在此示例中:
struct K1 {};
struct V1 {};
struct X: std::pair<K1, V1> {};
template<class A, class B> void f(std::pair<A, B> const & p);
int main()
{
f(X());
}
调用 f(X())
会将 A
推导为 K1
,将 B
推导为 V1
。如果我们有多个 std::pair
基类,我们可以将 A
固定为 K1
:
struct K1 {};
struct V1 {};
struct K2 {};
struct V2 {};
struct X: std::pair<K1, V1>, std::pair<K2, V2> {};
template<class B> void f(std::pair<K1, B> const & p);
int main()
{
f(X());
}
而 B
将被推导为 V1
。
我们可以通过返回所需的类型来检索推导结果:
template<class B> std::pair<K1, B> f(std::pair<K1, B> const & p);
然后使用 decltype(f(X()))
获取此返回类型。
如果 X
没有类型为 std::pair<K1, B>
的基类怎么办?推导将失败,我们将得到一个错误,表明 f(X())
无法被调用。为了避免这种情况,我们可以添加一个接受任何内容并返回 void
的 f
的重载。但在这种情况下,如果 X
有两个形式与第一个 f
重载匹配的基类,例如 std::pair<K1, Y>
和 std::pair<K1, Z>
,会发生什么?
推导将失败,将再次选择第二个重载,我们得到 void
。这就是为什么我们要求映射具有唯一键的原因。
下面是基于此技术实现的 mp_map_find
:
template<class M, class K> struct mp_map_find_impl;
template<class M, class K>
using mp_map_find = typename mp_map_find_impl<M, K>::type;
template<template<class...> class M, class... T, class K>
struct mp_map_find_impl<M<T...>, K>
{
struct U: mp_identity<T>... {};
template<template<class...> class L, class... U>
static mp_identity<L<K, U...>>
f( mp_identity<L<K, U...>>* );
static mp_identity<void> f( ... );
using V = decltype( f((U*)0) );
using type = typename V::type;
};
及其相应的编译时间:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,推导 |
0.3 |
0.7 |
1.8 |
3.6 |
6.4 |
10.4 |
16.2 |
DNF |
clang++ 3.5.1,推导 |
0.3 |
0.4 |
0.6 |
0.9 |
1.2 |
1.6 |
2.2 |
2.7 |
g++ 4.9.2,推导 |
0.3 |
0.5 |
0.9 |
1.6 |
2.3 |
3.4 |
4.7 |
6.3 |
这看起来可以发布了。
然而,该实现包含一个效率低下之处。如果我们先求值 mp_map_find<M, K1>
,然后求值 mp_map_find<M, K2>
,那么两个嵌套的 U
类型是相同的,因为它们仅依赖于 M
,但编译器不知道这一点,并且会单独实例化每个类型。我们应该将此类型移到 mp_map_find_impl
之外,以便它能够被重用:
template<class... T> struct mp_inherit: T... {};
template<class M, class K> struct mp_map_find_impl;
template<class M, class K>
using mp_map_find = typename mp_map_find_impl<M, K>::type;
template<template<class...> class M, class... T, class K>
struct mp_map_find_impl<M<T...>, K>
{
using U = mp_inherit<mp_identity<T>...>;
template<template<class...> class L, class... U>
static mp_identity<L<K, U...>>
f( mp_identity<L<K, U...>>* );
static mp_identity<void> f( ... );
using V = decltype( f((U*)0) );
using type = typename V::type;
};
(顺便说一句,这个优化也适用于我们基于 is_base_of
的 mp_contains
实现。)
在我们的基准测试上,编译时间的改进是可衡量的:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,deduction+mp_inherit |
0.3 |
0.6 |
1.4 |
2.6 |
4.5 |
7.1 |
10.7 |
DNF |
clang++ 3.5.1,deduction+mp_inherit |
0.3 |
0.4 |
0.6 |
0.8 |
1.0 |
1.4 |
1.8 |
2.2 |
g++ 4.9.2,deduction+mp_inherit |
0.3 |
0.4 |
0.6 |
0.9 |
1.3 |
1.8 |
2.3 |
2.9 |
mp_at
在涵盖了集合和映射之后,是时候处理向量了。对我们来说,向量就是列表,我们需要为其添加根据索引高效访问元素的能力。此访问器的惯用名称是 mp_at<L, I>
,其中 L
是列表,I
是表示索引的 integral_constant
。我们还将遵循 Boost.MPL 的约定,添加 mp_at_c<L, I>
,其中 I
的类型为 size_t
。
mp_at
的递归实现如下:
template<class L, std::size_t I> struct mp_at_c_impl;
template<class L, std::size_t I> using mp_at_c = typename mp_at_c_impl<L, I>::type;
template<class L, class I> using mp_at = typename mp_at_c_impl<L, I::value>::type;
template<template<class...> class L, class T1, class... T>
struct mp_at_c_impl<L<T1, T...>, 0>
{
using type = T1;
};
template<template<class...> class L, class T1, class... T, std::size_t I>
struct mp_at_c_impl<L<T1, T...>, I>
{
using type = mp_at_c<L<T...>, I-1>;
};
以下是调用 mp_at
N
次(第一个参数为大小为 N
的列表)的编译时间:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,递归 |
3.6 |
DNF |
||||||
clang++ 3.5.1,递归 |
1.0 |
5.1 |
15.3 |
DNF |
||||
g++ 4.9.2,递归 |
0.9 |
4.7 |
14.2 |
32.4 |
DNF |
为了改进这个糟糕的结果,我们将再次利用包展开到函数调用中,但以一种新颖的方式。假设我们需要访问第四个元素(I = 3
)。我们将生成函数签名:
template<class W> W f( void*, void*, void*, W*, ... );
然后,给定一个列表 L<T1, T2, T3, T4, T5, T6, T7>
,我们将求值表达式:
decltype( f( (T1*)0, (T2*)0, (T3*)0, (T4*)0, (T5*)0, (T6*)0, (T7*)0 ) )
三个 void*
参数将处理前三个元素,W
将被推导为第四个元素,省略号将处理其余部分。
下面显示了一个基于此技术的有效实现:
// mp_repeat_c
template<std::size_t N, class... T> struct mp_repeat_c_impl
{
using _l1 = typename mp_repeat_c_impl<N/2, T...>::type;
using _l2 = typename mp_repeat_c_impl<N%2, T...>::type;
using type = mp_append<_l1, _l1, _l2>;
};
template<class... T> struct mp_repeat_c_impl<0, T...>
{
using type = mp_list<>;
};
template<class... T> struct mp_repeat_c_impl<1, T...>
{
using type = mp_list<T...>;
};
template<std::size_t N, class... T> using mp_repeat_c =
typename mp_repeat_c_impl<N, T...>::type;
// mp_at
template<class L, class L2> struct mp_at_c_impl;
template<template<class...> class L, class... T,
template<class...> class L2, class... U>
struct mp_at_c_impl<L<T...>, L2<U...>>
{
template<class W> static W f( U*..., W*, ... );
using R = decltype( f( (mp_identity<T>*)0 ... ) );
using type = typename R::type;
};
template<class L, std::size_t I> using mp_at_c =
typename mp_at_c_impl<L, mp_repeat_c<I, void>>::type;
template<class L, class I> using mp_at = mp_at_c<L, I::value>;
下表中的编译时间表明它足以满足大多数实际需求。
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,void* |
0.4 |
1.1 |
2.4 |
4.7 |
DNF |
|||
clang++ 3.5.1,void* |
0.4 |
0.7 |
1.2 |
1.9 |
2.7 |
3.8 |
5.0 |
6.6 |
g++ 4.9.2,void* |
0.3 |
0.5 |
0.9 |
1.3 |
2.1 |
3.0 |
4.2 |
5.5 |
那么,mp_at
就算完成了吗?
让我们尝试另一种方法——将输入列表 [T1, T2, T3]
转换为映射 [[0, T1], [1, T2], [2, T3]]
,然后使用 mp_map_find
进行查找:
// mp_map_from_list
template<class L, class S> struct mp_map_from_list_impl;
template<template<class...> class L, class... T, std::size_t... J>
struct mp_map_from_list_impl<L<T...>, integer_sequence<std::size_t, J...>>
{
using type = mp_list<mp_list<mp_size_t<J>, T>...>;
};
template<class L> using mp_map_from_list = typename mp_map_from_list_impl<L,
make_index_sequence<mp_size<L>::value>>::type;
// mp_at
template<class L, std::size_t I> struct mp_at_c_impl
{
using map = mp_map_from_list<L>;
using type = mp_second<mp_map_find<map, mp_size_t<I>>>;
};
template<class L, std::size_t I> using mp_at_c = typename mp_at_c_impl<L, I>::type;
template<class L, class I> using mp_at = typename mp_at_c_impl<L, I::value>::type;
乍一看,这看起来很荒谬,但元编程有其自身的规则。我们来测量一下:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,map |
0.3 |
0.7 |
1.5 |
2.9 |
5.0 |
7.8 |
11.9 |
DNF |
clang++ 3.5.1,map |
0.3 |
0.4 |
0.6 |
0.8 |
1.1 |
1.5 |
1.8 |
2.3 |
g++ 4.9.2,map |
0.3 |
0.4 |
0.7 |
1.0 |
1.4 |
1.9 |
2.5 |
3.2 |
令人惊讶的是,这是最好的实现。
mp_drop
事实证明,我们并不需要 void*
技巧来处理 mp_at
,但我将展示一个需要它的例子:mp_drop
。mp_drop<L, N>
返回列表 L
去掉前 N
个元素;换句话说,它丢弃前 N
个元素(可能被扔进废纸篓),然后返回剩下的部分。
为了实现 mp_drop
,我们只需要将上面的
template<class W> W f( void*, void*, void*, W*, ... );
修改为返回剩余的元素,而不是仅仅一个:
template<class... W> mp_list<W> f( void*, void*, void*, W*... );
添加必要的 mp_identity
调味剂,得到以下有效实现:
template<class L, class L2> struct mp_drop_c_impl;
template<template<class...> class L, class... T,
template<class...> class L2, class... U>
struct mp_drop_c_impl<L<T...>, L2<U...>>
{
template<class... W> static mp_identity<L<W...>> f( U*..., mp_identity<W>*... );
using R = decltype( f( (mp_identity<T>*)0 ... ) );
using type = typename R::type;
};
template<class L, std::size_t N> using mp_drop_c =
typename mp_drop_c_impl<L, mp_repeat_c<N, void>>::type;
template<class L, class N> using mp_drop = mp_drop_c<L, N::value>;
我将省略此处的递归实现和性能比较。我们可以 pretty much 知道谁将获胜,以及胜出多少。
mp_find_index
我将引起您注意的最后一个算法是 mp_find_index
。mp_find_index<L, V>
返回一个类型为 size_t
的整数常量,其值为 V
在 L
中首次出现的索引。如果 V
不在 L
中,则返回值是 L
的大小。
一如既往,我们先从递归实现开始:
template<class L, class V> struct mp_find_index_impl;
template<class L, class V> using mp_find_index = typename mp_find_index_impl<L, V>::type;
template<template<class...> class L, class V> struct mp_find_index_impl<L<>, V>
{
using type = mp_size_t<0>;
};
template<template<class...> class L, class... T, class V>
struct mp_find_index_impl<L<V, T...>, V>
{
using type = mp_size_t<0>;
};
template<template<class...> class L, class T1, class... T, class V>
struct mp_find_index_impl<L<T1, T...>, V>
{
using type = mp_size_t<1 + mp_find_index<L<T...>, V>::value>;
};
并且,一如既往,我们将继续进行调用 mp_find_index
N
次(列表大小为 N
)的编译时间:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,递归 |
3.5 |
DNF |
||||||
clang++ 3.5.1,递归 |
1.1 |
5.5 |
DNF |
|||||
g++ 4.9.2,递归 |
0.8 |
4.6 |
13.6 |
DNF |
这里我们能做什么?
让我们回到 mp_contains
,看看“mp_count/cx_plus2”实现,我们之前为了继承而否决了它。它构建了一个 constexpr
布尔数组,并在 constexpr
函数中对其求和。我们可以在这里做同样的事情,除了我们不对数组求和,而是找到第一个 true 值的索引:
template<class L, class V> struct mp_find_index_impl;
template<class L, class V> using mp_find_index = typename mp_find_index_impl<L, V>::type;
template<template<class...> class L, class V> struct mp_find_index_impl<L<>, V>
{
using type = mp_size_t<0>;
};
constexpr std::size_t cx_find_index( bool const * first, bool const * last )
{
return first == last || *first? 0: 1 + cx_find_index( first + 1, last );
}
template<template<class...> class L, class... T, class V>
struct mp_find_index_impl<L<T...>, V>
{
static constexpr bool _v[] = { std::is_same<T, V>::value... };
using type = mp_size_t< cx_find_index( _v, _v + sizeof...(T) ) >;
};
此版本的性能如下:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
clang++ 3.5.1,constexpr |
0.5 |
1.3 |
2.9 |
DNF |
||||
g++ 4.9.2,constexpr |
0.5 |
1.4 |
3.1 |
5.5 |
8.7 |
13.0 |
18.0 |
DNF |
虽然不理想,但比我们被动挨打的递归版本要好得多。但是,如果我们的首选编译器是 VC++ 2013,我们就不能使用 constexpr
。
我们可以尝试一个类似方向的实现,但是将 constexpr
函数替换为普通的元编程:
template<class L, class V> struct mp_find_index_impl;
template<class L, class V> using mp_find_index = typename mp_find_index_impl<L, V>::type;
template<template<class...> class L, class V> struct mp_find_index_impl<L<>, V>
{
using type = mp_size_t<0>;
};
template<bool...> struct find_index_impl_;
template<> struct find_index_impl_<>
{
static const std::size_t value = 0;
};
template<bool B1, bool... R> struct find_index_impl_<B1, R...>
{
static const std::size_t value = B1? 0: 1 + find_index_impl_<R...>::value;
};
template<bool B1, bool B2, bool B3, bool B4, bool B5,
bool B6, bool B7, bool B8, bool B9, bool B10, bool... R>
struct find_index_impl_<B1, B2, B3, B4, B5, B6, B7, B8, B9, B10, R...>
{
static const std::size_t value = B1? 0: B2? 1: B3? 2: B4? 3: B5? 4:
B6? 5: B7? 6: B8? 7: B9? 8: B10? 9: 10 + find_index_impl_<R...>::value;
};
template<template<class...> class L, class... T, class V>
struct mp_find_index_impl<L<T...>, V>
{
using type = mp_size_t<find_index_impl_<std::is_same<T, V>::value...>::value>;
};
这仍然是递归的,所以我们不期望奇迹,但测量一下也无妨:
N=100 | N=200 | N=300 | N=400 | N=500 | N=600 | N=700 | N=800 | |
---|---|---|---|---|---|---|---|---|
VC++ 2013,bool… |
4.7 |
94.5 |
488.3 |
XFA |
||||
clang++ 3.5.1,bool… |
0.6 |
2.2 |
5.8 |
12.0 |
21.7 |
35.2 |
DNF |
|
g++ 4.9.2,bool… |
0.6 |
2.4 |
6.5 |
13.2 |
23.8 |
39.1 |
59.0 |
DNF |
(其中 XFA 表示“实验者睡着了”。)
这对于 VC++ 2013 和 Clang 来说是一个有趣的权衡。一方面,这个实现更慢;另一方面,它不容易导致编译器崩溃。选择哪一个取决于个人喜好以及对操作长度为 300 的类型列表需求的严肃评估。
请注意,一旦有了 mp_drop
和 mp_find_index
,我们就可以通过使用 mp_drop<L, mp_find_index<L, V>>
来推导出 mp_find<L, V>
算法。该算法返回从 V
的第一个出现开始的 L
的后缀(如果存在),否则返回一个空列表。
结论
在本文中,我展示了允许我们将类型列表视为集合、映射和向量的高效算法,并在此过程中展示了各种 C++11 的实现技术。