在本章中,我们将介绍:
- 使用类型的类型向量
- 操纵类型向量
- 在编译时获取函数的结果类型
- 制作高阶元功能
- 懒洋洋地评估元功能
- 将所有元组元素转换为字符串
- 拆分元组
- 在 C++ 14 中操作异构容器
这一章致力于介绍一些很酷也很难理解的元编程方法。这些方法不是日常使用的,但是它们可能对泛型库的开发有真正的帮助。
第 4 章编译时技巧已经介绍了元编程的基础知识。为了更好的理解,建议阅读它。在这一章中,我们将更深入地了解如何将多个类型打包到一个类似类型的元组中。我们将制作用于操作类型集合的函数,我们将看到编译时集合的类型可能如何改变,以及编译时技巧如何与运行时混合。所有这些都是元编程。
系好安全带,准备好,我们走...!
有些情况下,处理所有模板参数就像在容器中一样非常好。想象我们正在写一些东西,比如Boost.Variant:
#include <boost/mpl/aux_/na.hpp>
// boost::mpl::na == n.a. == not available
template <
class T0 = boost::mpl::na,
class T1 = boost::mpl::na,
class T2 = boost::mpl::na,
class T3 = boost::mpl::na,
class T4 = boost::mpl::na,
class T5 = boost::mpl::na,
class T6 = boost::mpl::na,
class T7 = boost::mpl::na,
class T8 = boost::mpl::na,
class T9 = boost::mpl::na
>
struct variant;前面的代码是以下所有有趣任务开始发生的地方:
- 我们如何从所有类型中移除常量和变量限定符?
- 我们如何删除重复的类型?
- 我们如何得到所有类型的尺寸?
- 怎样才能得到输入参数的最大值?
所有这些任务都可以使用Boost.MPL轻松解决。
本食谱需要第 4 章编译时技巧的基本知识。阅读前积累一些勇气——这个食谱中会有很多元编程。
我们已经看到了如何在编译时操作一个类型。为什么我们不能更进一步,在一个数组中组合多种类型,并对该数组的每个元素执行操作呢?
- 首先,让我们将所有类型打包到
Boost.MPL类型的容器中:
#include <boost/mpl/vector.hpp>
template <
class T0, class T1, class T2, class T3, class T4,
class T5, class T6, class T7, class T8, class T9
>
struct variant {
typedef boost::mpl::vector<
T0, T1, T2, T3, T4, T5, T6, T7, T8, T9
> types;
};- 让我们让我们的例子不那么抽象,看看如果我们指定类型,它是如何工作的:
#include <string>
struct declared{ unsigned char data[4096]; };
struct non_declared;
typedef variant<
volatile int,
const int,
const long,
declared,
non_declared,
std::string
>::types types;- 我们可以在编译时检查一切。让我们断言类型不是空的:
#include <boost/static_assert.hpp>
#include <boost/mpl/empty.hpp>
BOOST_STATIC_ASSERT((!boost::mpl::empty<types>::value)); - 我们也可以检查,例如,
non_declared类型仍然在索引4位置:
#include <boost/mpl/at.hpp>
#include <boost/type_traits/is_same.hpp>
BOOST_STATIC_ASSERT((boost::is_same<
non_declared,
boost::mpl::at_c<types, 4>::type
>::value));- 最后一种仍然是
std::string:
#include <boost/mpl/back.hpp>
BOOST_STATIC_ASSERT((boost::is_same<
boost::mpl::back<types>::type,
std::string
>::value));- 我们可能会进行一些改造。让我们从移除常量和变量限定词开始:
#include <boost/mpl/transform.hpp>
#include <boost/type_traits/remove_cv.hpp>
typedef boost::mpl::transform<
types,
boost::remove_cv<boost::mpl::_1>
>::type noncv_types;- 下面是我们删除重复类型的方法:
#include <boost/mpl/unique.hpp>
typedef boost::mpl::unique<
noncv_types,
boost::is_same<boost::mpl::_1, boost::mpl::_2>
>::type unique_types;- 我们可以检查向量只包含
5类型:
#include <boost/mpl/size.hpp>
BOOST_STATIC_ASSERT((boost::mpl::size<unique_types>::value == 5));- 下面是我们如何计算每个元素的大小:
// Without this we'll get an error:
// "use of undefined type 'non_declared'"
struct non_declared{};
#include <boost/mpl/sizeof.hpp>
typedef boost::mpl::transform<
unique_types,
boost::mpl::sizeof_<boost::mpl::_1>
>::type sizes_types;- 这是如何从
sizes_type类型中获得最大尺寸:
#include <boost/mpl/max_element.hpp>
typedef boost::mpl::max_element<sizes_types>::type max_size_type;我们可以断言,类型的最大大小等于结构的声明大小,这一定是我们示例中最大的一个:
BOOST_STATIC_ASSERT(max_size_type::type::value == sizeof(declared)); boost::mpl::vector类是一个保存类型的编译时容器。更准确地说,它是一个保存类型的类型。我们不做它的例子;相反,我们只是在typedef s 中使用它。
与标准库容器不同,Boost.MPL容器没有成员方法。相反,方法在单独的头中声明。所以要使用一些方法,我们需要:
- 包括正确的标题。
- 通常通过将容器指定为第一个参数来调用该方法。
我们已经在第四章、编译时技巧中看到了元功能。我们使用了一些熟悉的Boost.TypeTraits库中的元功能(比如boost::is_same)。
所以,在步骤3步骤 4 ,以及步骤 5 中,我们只是在为我们的容器类型调用元功能。
最难的部分来了!
占位符被Boost.MPL库广泛用于组合元功能:
typedef boost::mpl::transform<
types,
boost::remove_cv<boost::mpl::_1>
>::type noncv_types;这里,boost::mpl::_1是一个占位符,整个表达式的意思是,对于types中的每个类型,做boost::remove_cv<>::type并将该类型推回到结果向量。通过::type返回结果向量。
让我们移至步骤 7 。这里,我们使用boost::is_same<boost::mpl::_1, boost::mpl::_2>模板参数为boost::mpl::unique指定一个比较元函数,其中boost::mpl::_1和boost::mpl::_2是占位符。你可能会发现它类似于boost::bind(std::equal_to(), _1, _2),而步骤 7 中的整个表达式类似于下面的伪代码:
std::vector<type> t; // 't' stands for 'types'.
std::unique(t.begin(), t.end(), boost::bind(std::equal_to<type>(), _1, _2)); 在第步第步中,有一些有趣的东西需要更好的理解。在前面的代码中,sizes_types不是一个值的向量,而是一个整数常量的向量——代表数字的类型。sizes_types typedef实际上是以下类型:
struct boost::mpl::vector<
struct boost::mpl::size_t<4>,
struct boost::mpl::size_t<4>,
struct boost::mpl::size_t<4096>,
struct boost::mpl::size_t<1>,
struct boost::mpl::size_t<32>
>最后一步现在必须明确。它只是从sizes_types typedef中获取最大元素。
We may use the Boost.MPL metafunctions at any place where typedefs are allowed.
Boost.MPL库的使用导致更长的编译时间,但是让你能够用类型做任何你想做的事情。它不会增加运行时开销,甚至不会向生成的二进制文件中添加一条指令。C++ 17 没有Boost.MPL类,Boost.MPL没有使用现代 C++ 的特性,比如变量模板。这使得Boost.MPL的编译时间在 C++ 11 编译器上不会尽可能短,但是使得该库在 C++ 03 编译器上可用。
- 有关元编程的信息基础,请参见第 4 章、编译时技巧
- 操纵类型向量配方将为您提供更多关于元编程和
Boost.MPL库的信息 - 更多示例和完整参考请参见
Boost.MPL官方文档,位于http://boost.org/libs/mpl
该配方的任务是根据第二个boost::mpl::vector功能的内容修改一个boost::mpl::vector功能的内容。我们将第二个向量称为修饰符向量,每个修饰符可能有以下类型:
// Make unsigned.
struct unsigne; // Not a typo: `unsigned` is a keyword, we can not use it.
// Make constant.
struct constant;
// Otherwise we do not change type.
struct no_change;那么,我们从哪里开始呢?
需要Boost.MPL的基础知识。阅读使用类型的类型向量配方和第 4 章,*编译时技巧,*可能会有所帮助。
这个方法与前一个类似,但是它也使用条件编译时语句。准备好,这不容易!
- 我们将从标题开始:
// We'll need this at step 3.
#include <boost/mpl/size.hpp>
#include <boost/type_traits/is_same.hpp>
#include <boost/static_assert.hpp>
// We'll need this at step 4.
#include <boost/mpl/if.hpp>
#include <boost/type_traits/make_unsigned.hpp>
#include <boost/type_traits/add_const.hpp>
// We'll need this at step 5.
#include <boost/mpl/transform.hpp>- 现在,让我们将所有元编程的魔力都放在结构中,以便更简单地重用:
template <class Types, class Modifiers>
struct do_modifications {- 最好检查传递的向量是否具有相同的大小:
BOOST_STATIC_ASSERT((boost::is_same<
typename boost::mpl::size<Types>::type,
typename boost::mpl::size<Modifiers>::type
>::value));- 现在,让我们来处理修改元功能:
typedef boost::mpl::if_<
boost::is_same<boost::mpl::_2, unsigne>,
boost::make_unsigned<boost::mpl::_1>,
boost::mpl::if_<
boost::is_same<boost::mpl::_2, constant>,
boost::add_const<boost::mpl::_1>,
boost::mpl::_1
>
> binary_operator_t;- 最后一步:
typedef typename boost::mpl::transform<
Types,
Modifiers,
binary_operator_t
>::type type;
};我们现在将运行一些测试,并确保我们的元功能运行良好:
#include <boost/mpl/vector.hpp>
#include <boost/mpl/at.hpp>
typedef boost::mpl::vector<
unsigne, no_change, constant, unsigne
> modifiers;
typedef boost::mpl::vector<
int, char, short, long
> types;
typedef do_modifications<types, modifiers>::type result_type;
BOOST_STATIC_ASSERT((boost::is_same<
boost::mpl::at_c<result_type, 0>::type,
unsigned int
>::value));
BOOST_STATIC_ASSERT((boost::is_same<
boost::mpl::at_c<result_type, 1>::type,
char
>::value));
BOOST_STATIC_ASSERT((boost::is_same<
boost::mpl::at_c<result_type, 2>::type,
const short
>::value));
BOOST_STATIC_ASSERT((boost::is_same<
boost::mpl::at_c<result_type, 3>::type,
unsigned long
>::value));在步骤 *3 中,*我们断言大小是相等的,但是我们以一种不同寻常的方式做到了。boost::mpl::size<Types>::type元函数实际上返回一个整数常量struct boost::mpl::long_<4>,所以在静态断言中,我们实际上比较的是两种类型,而不是两个数字。这可以用一种更熟悉的方式改写:
BOOST_STATIC_ASSERT((
boost::mpl::size<Types>::type::value
==
boost::mpl::size<Modifiers>::type::value
));Notice the typename keyword we use. Without it, the compiler won't be able to decide if ::type is actually a type or some variable. Previous recipes did not require it, because parameters for the metafunction were fully known at the point where we were using them. But in this recipe, parameter for the metafunction is a template.
我们先来看看第五步,再来处理第四步。在步骤 5 中,我们提供了从步骤 4 到boost::mpl::transform元功能的Types、Modifiers和binary_operator_t参数。这个元函数相当简单——对于每个传递的向量,它接受一个元素并将其传递给第三个参数——二进制元函数。如果我们用伪代码重写它,它将如下所示:
void boost_mpl_transform_pseoudo_code() {
vector result;
for (std::size_t i = 0; i < Types.size(); ++ i) {
result.push_back(
binary_operator_t(Types[i], Modifiers[i])
);
}
return result;
}第四步可能会使某人头部受伤。在这一步中,我们编写一个元函数,为来自Types和Modifiers向量的每一对类型调用(参见前面的伪代码):
typedef boost::mpl::if_<
boost::is_same<boost::mpl::_2, unsigne>,
boost::make_unsigned<boost::mpl::_1>,
boost::mpl::if_<
boost::is_same<boost::mpl::_2, constant>,
boost::add_const<boost::mpl::_1>,
boost::mpl::_1
>
> binary_operator_t;我们已经知道,boost::mpl::_2和boost::mpl::_1是占位符。在本食谱中,_1是来自Types向量的类型的占位符,_2是来自Modifiers向量的类型的占位符。
所以,整个元功能是这样工作的:
- 将传递给它的第二个参数(通过
_2)与unsigned类型进行比较。 - 如果类型相等,使传递给它的第一个参数(通过
_1)无符号并返回该类型。 - 否则,它会将传递给它的第二个参数(通过
_2)与常量类型进行比较。 - 如果类型相等,它会使传递给它的第一个参数(通过
_1)保持不变,并返回该类型。 - 否则,返回传递给它的第一个参数(通过
_1)。
我们在构建这个元功能时需要非常小心。应额外注意不要在结尾调用::type:
>::type binary_operator_t; // INCORRECT! 如果我们调用::type,编译器会在此时尝试对二进制运算符求值,这将导致编译错误。在伪代码中,这样的尝试看起来像这样:
binary_operator_t foo;
// Attempt to call binary_operator_t::operator() without parameters,
// when it has only two parameters overloads.
foo(); 使用元功能需要一些练习。即使你卑微的仆人也不能从第一次尝试就正确地编写一些函数(尽管第二次和第三次尝试也不好)。不要害怕或困惑去实验!
Boost.MPL库不是 C++ 17 的一部分,不使用现代 C++ 特性,但可以与 C++ 11 变量模板一起使用:
template <class... T>
struct vt_example {
typedef typename boost::mpl::vector<T...> type;
};
BOOST_STATIC_ASSERT((boost::is_same<
boost::mpl::at_c<vt_example<int, char, short>::type, 0>::type,
int
>::value)); 和往常一样,元函数不会向生成的二进制文件中添加一条指令,也不会使性能变差。然而,使用它们可以使您的代码更好地适应特定的情况。
- 从头开始阅读本章,以获得更多简单的
Boost.MPL用法示例 - 参见第 4 章、编译时技巧,特别是为模板参数配方选择最佳运算符,其中包含类似于
binary_operator_t元功能的代码 Boost.MPL的官方文档在http://boost.org/libs/mpl有更多的例子和完整的目录
C++ 11 增加了许多好的特性来简化元编程。一个这样的特性是替代函数语法。它允许推导模板函数的结果类型。这里有一个例子:
template <class T1, class T2>
auto my_function_cpp11(const T1& v1, const T2& v2)
-> decltype(v1 + v2)
{
return v1 + v2;
}它允许我们更容易地编写通用函数:
#include <cassert>
struct s1 {};
struct s2 {};
struct s3 {};
inline s3 operator + (const s1& /*v1*/, const s2& /*v2*/) {
return s3();
}
inline s3 operator + (const s2& /*v1*/, const s1& /*v2*/) {
return s3();
}
int main() {
s1 v1;
s2 v2;
s3 res0 = my_function_cpp11(v1, v2);
assert(my_function_cpp11('\0', 1) == 1);
}但是,Boost 有很多这样的功能,它不需要 C++ 11 就能工作。这怎么可能,我们怎么才能做出 C++ 03 版本的my_function_cpp11函数?
这个食谱需要 C++ 和模板的基本知识。
C++ 11 极大地简化了元编程。必须用 C++ 03 编写大量代码,以使其接近替代函数语法:
- 我们必须包含以下标题:
#include <boost/type_traits/common_type.hpp>- 现在,让我们在
result_of命名空间中为任何类型创建一个元函数:
namespace result_of {
template <class T1, class T2>
struct my_function_cpp03 {
typedef typename boost::common_type<T1, T2>::type type;
};- 并将其专门化为
s1和s2类型:
template <>
struct my_function_cpp03<s1, s2> {
typedef s3 type;
};
template <>
struct my_function_cpp03<s2, s1> {
typedef s3 type;
};
} // namespace result_of- 现在我们准备编写
my_function_cpp03函数:
template <class T1, class T2>
typename result_of::my_function_cpp03<T1, T2>::type
my_function_cpp03(const T1& v1, const T2& v2)
{
return v1 + v2;
}就这样!现在,我们可以像使用 C++ 11 一样使用这个函数:
int main() {
s1 v1;
s2 v2;
s3 res1 = my_function_cpp03(v1, v2);
assert(my_function_cpp03('\0', 1) == 1);
}这个方法的主要思想是,我们可以制作一个特殊的元函数来推导结果类型。这样的技术在 Boost 库中随处可见,例如在Boost.Variant的boost::get<>实现中,或者在Boost.Fusion的几乎任何功能中。
现在,让我们一步一步来。result_of命名空间只是某种传统,但你可以使用自己的命名空间,这并不重要。boost::common_type<>元功能推导出几种类型中常见的一种类型,所以我们将其用于一般情况。我们还为s1和s2类型添加了两个模板专门化的result_of::my_function_cpp03结构。
The disadvantage of writing metafunctions in C++ 03 is that sometimes we are required to write a lot. Compare the amount of code for my_function_cpp11 and my_function_cpp03 including the result_of namespace to feel the difference.
当元函数准备好了,我们可以不用 C++ 11 推导出结果类型:
template <class T1, class T2>
typename result_of::my_function_cpp03<T1, T2>::type
my_function_cpp03(const T1& v1, const T2& v2);这种技术不会增加运行时开销,但可能会稍微降低编译速度。您也可以在现代 C++ 编译器上使用它。
- 食谱启用积分类型的模板化函数、禁用实数类型的模板化函数和从第 4 章、编译时技巧中为模板参数选择最佳运算符,将为您提供更多关于
Boost.TypeTraits和元编程的信息 - 考虑
Boost.TypeTraits的官方文档,了解更多关于在http://boost.org/libs/type_traits准备好的元功能的信息
接受其他函数作为输入参数的函数或返回其他函数的函数称为高阶函数。例如,以下函数是高阶函数:
typedef void(*function_t)(int);
function_t higher_order_function1();
void higher_order_function2(function_t f);
function_t higher_order_function3(function_t f); f); 我们已经在本章使用类型的类型向量的食谱和操作类型的向量的食谱中看到了更高阶的元功能,我们使用了boost::mpl::transform。
在这个食谱中,我们将尝试制作我们自己的高阶元功能coalesce,它接受两种类型和两种元功能。coalesce元功能将第一个类型参数应用于第一个元功能,并将结果类型与boost::mpl::false_类型进行比较。如果结果类型是boost::mpl::false_类型,则返回将第二个 type-参数应用于第二个图元函数的结果,否则返回第一个结果类型:
template <class Param1, class Param2, class Func1, class Func2>
struct coalesce;这个食谱(和章节)是一个棘手的。强烈建议从头开始阅读这一章。
Boost.MPL元功能实际上是可以作为模板参数轻松传递的结构。最难的是正确使用它:
- 我们需要以下标题来编写高阶元函数:
#include <boost/mpl/apply.hpp>
#include <boost/mpl/if.hpp>
#include <boost/type_traits/is_same.hpp>- 下一步是评估我们的功能:
template <class Param1, class Param2, class Func1, class Func2>
struct coalesce {
typedef typename boost::mpl::apply<Func1, Param1>::type type1;
typedef typename boost::mpl::apply<Func2, Param2>::type type2;- 现在,我们需要选择正确的结果类型:
typedef typename boost::mpl::if_<
boost::is_same< boost::mpl::false_, type1>,
type2,
type1
>::type type;
};就这样!我们已经完成了高阶元功能!现在,我们可以这样使用它:
#include <boost/static_assert.hpp>
#include <boost/mpl/not.hpp>
#include <boost/mpl/next.hpp>
using boost::mpl::_1;
using boost::mpl::_2;
typedef coalesce<
boost::mpl::true_,
boost::mpl::int_<5>,
boost::mpl::not_<_1>,
boost::mpl::next<_1>
>::type res1_t;
BOOST_STATIC_ASSERT((res1_t::value == 6));
typedef coalesce<
boost::mpl::false_,
boost::mpl::int_<5>,
boost::mpl::not_<_1>,
boost::mpl::next<_1>
>::type res2_t;
BOOST_STATIC_ASSERT((res2_t::value));编写高阶元函数的主要问题是如何处理占位符。这就是为什么我们不直接叫Func1<Param1>::type的原因。相反,我们必须使用boost::mpl::apply元函数,它接受一个函数和最多五个传递给这个函数的参数。
You may configure boost::mpl::apply to accept even more parameters, defining the BOOST_MPL_LIMIT_METAFUNCTION_ARITY macro to the required amount of parameters, for example, to 6.
C++ 11 没有任何接近Boost.MPL库的东西来应用一个元函数。
现代 C++ 有一大堆可能帮助你实现Boost.MPL功能的特性。例如,C++ 11 有一个<type_traits>头和基本 常量表达式支持。C++ 14 有一个扩展常量表达式支持,在 C++ 17 中有一个std::apply函数可以处理元组,并且可以在常量表达式中使用。此外,在 C++ 17 中,lambdas 默认为 constexpr,并且有一个 if constexpr (expr)。
Writing your own solution would waste a lot of time and probably would not work on older compilers. So, Boost.MPL still remains one of the most suitable solutions for metaprogramming.
请参阅官方文档,尤其是教程部分,了解更多关于http://boost.org/libs/mpl的Boost.MPL信息。
懒惰求值意味着在我们真正需要它的结果之前,不会调用函数。为了写出好的元功能,强烈推荐了解这个方法。懒惰评估的重要性将在下面的例子中展示。
假设我们正在编写一些元函数,接受一个函数Func、一个参数Param和一个条件Cond。如果将Cond应用于Param返回false,则该函数的结果类型必须是fallback类型,否则结果必须是应用于Param的Func:
struct fallback;
template <
class Func,
class Param,
class Cond,
class Fallback = fallback>
struct apply_if; 这个元功能是我们离不开懒评价的地方,因为如果不满足Cond可能就无法将Func应用到Param上。这样的尝试总是会导致编译失败,Fallback永远不会返回。
强烈推荐阅读第 4 章编译时技巧。然而,良好的元编程知识应该就足够了。
盯紧小细节,比如不要在例子中调用::type:
- 我们需要以下标题:
#include <boost/mpl/apply.hpp>
#include <boost/mpl/eval_if.hpp>
#include <boost/mpl/identity.hpp>- 函数的开头很简单:
template <class Func, class Param, class Cond, class Fallback>
struct apply_if {
typedef typename boost::mpl::apply<
Cond, Param
>::type condition_t;- 我们在这里要小心:
typedef boost::mpl::apply<Func, Param> applied_type; - 计算表达式时,必须格外小心:
typedef typename boost::mpl::eval_if_c<
condition_t::value,
applied_type,
boost::mpl::identity<Fallback>
>::type type;
};就这样!现在我们可以像这样自由使用它:
#include <boost/static_assert.hpp>
#include <boost/type_traits/is_integral.hpp>
#include <boost/type_traits/make_unsigned.hpp>
#include <boost/type_traits/is_same.hpp>
using boost::mpl::_1;
using boost::mpl::_2;
typedef apply_if<
boost::make_unsigned<_1>,
int,
boost::is_integral<_1>
>::type res1_t;
BOOST_STATIC_ASSERT((
boost::is_same<res1_t, unsigned int>::value
));
typedef apply_if<
boost::make_unsigned<_1>,
float,
boost::is_integral<_1>
>::type res2_t;
BOOST_STATIC_ASSERT((
boost::is_same<res2_t, fallback>::value
));这个食谱的主要思想是,如果条件为false,我们就不能执行元功能,因为当条件为false时,有可能该类型的元功能不能应用:
// Will fail with static assertion somewhere deeply in the implementation
// of boost::make_unsigned<_1> if we do not evaluate the function lazily.
typedef apply_if<
boost::make_unsigned<_1>,
float,
boost::is_integral<_1>
>::type res2_t;
BOOST_STATIC_ASSERT((
boost::is_same<res2_t, fallback>::value
));那么,我们如何懒懒地评价一个元功能呢?
如果无法访问元函数的内部类型或值,编译器会查看元函数内部。换句话说,当我们试图通过::获取元函数的一个成员时,编译器试图编译该元函数。这可以是打给::type或::value的电话。这就是apply_if的错误版本的样子:
template <class Func, class Param, class Cond, class Fallback>
struct apply_if {
typedef typename boost::mpl::apply<
Cond, Param
>::type condition_t;
// Incorrect: metafunction is evaluated when `::type` called.
typedef typename boost::mpl::apply<Func, Param>::type applied_type;
typedef typename boost::mpl::if_c<
condition_t::value,
applied_type,
boost::mpl::identity<Fallback>
>::type type;
};这与我们的例子不同,我们没有在第 3 步调用::type,而是使用eval_if_c实现了第 4 步,只为其中一个参数调用::type。boost::mpl::eval_if_c元功能是这样实现的:
template<bool C, typename F1, typename F2>
struct eval_if_c {
typedef typename if_c<C,F1,F2>::type f_;
typedef typename f_::type type; // call `::type` only for one parameter
};因为boost::mpl::eval_if_c调用::type表示成功条件,fallback没有::type,所以我们需要将fallback包装到boost::mpl::identity类中。这个类非常简单,但是很有用,它通过一个::type调用返回它的模板参数,并且不做任何事情:
template <class T>
struct identity {
typedef T type;
}; 正如我们已经提到的,C++ 11 没有Boost.MPL类,但是我们可以像boost::mpl::identity<T>一样用单个参数使用std::common_type<T>。
和往常一样,元函数不会在输出二进制文件中添加一行,您可以根据需要多次使用元函数。编译时做得越多,留给运行时的时间就越少。
boost::mpl::identity类型可用于禁用模板功能的参数相关查找 ( ADL )。参见<boost/implicit_cast.hpp>标题中的boost::implicit_cast来源。- 从头开始读这一章,在http://boost.org/libs/mpl阅读
Boost.MPL的官方文件可能会有帮助。
这个食谱和下一个食谱致力于混合编译时和运行时特性。我们将使用Boost.Fusion库,看看它能做什么。
还记得我们在第一章中讨论的元组和数组吗?现在,我们想编写一个单一的函数,可以将元组和数组的元素流式传输到字符串中。
您应该知道boost::tuple和boost::array类以及boost::lexical_cast函数。
我们已经知道了几乎所有将在这个食谱中使用的函数和类。我们只需要把它们都聚集在一起:
- 我们需要编写一个将任何类型转换为字符串的函子:
#include <boost/lexical_cast.hpp>
#include <boost/noncopyable.hpp>
struct stringize_functor: boost::noncopyable {
private:
std::string& result;
public:
explicit stringize_functor(std::string& res)
: result(res)
{}
template <class T>
void operator()(const T& v) const {
result += boost::lexical_cast<std::string>(v);
}
};- 现在是代码的棘手部分:
#include <boost/fusion/include/for_each.hpp>
template <class Sequence>
std::string stringize(const Sequence& seq) {
std::string result;
boost::fusion::for_each(seq, stringize_functor(result));
return result;
}仅此而已!现在,我们可以转换任何我们想要的字符串:
#include <iostream>
#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/adapted/boost_tuple.hpp>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/fusion/adapted/boost_array.hpp>
struct cat{};
std::ostream& operator << (std::ostream& os, const cat& ) {
return os << "Meow! ";
}
int main() {
boost::fusion::vector<cat, int, std::string> tup1(cat(), 0, "_0");
boost::tuple<cat, int, std::string> tup2(cat(), 0, "_0");
std::pair<cat, cat> cats;
boost::array<cat, 10> many_cats;
std::cout << stringize(tup1) << '\n'
<< stringize(tup2) << '\n'
<< stringize(cats) << '\n'
<< stringize(many_cats) << '\n';
}前面的示例输出了以下内容:
Meow! 0_0
Meow! 0_0
Meow! Meow!
Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow!stringize函数的主要问题是boost::tuple和std::pair都没有begin()和end()方法,所以我们不能调用std::for_each。这就是Boost.Fusion介入的地方。
Boost.Fusion库包含了许多很棒的算法,可以在编译时操纵结构。
boost::fusion::for_each函数迭代序列的元素,并为每个元素应用一个函子。
请注意,我们包括:
#include <boost/fusion/adapted/boost_tuple.hpp>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/fusion/adapted/boost_array.hpp> 这是必需的,因为默认情况下Boost.Fusion只对自己的类起作用。Boost.Fusion有自己的元组类boost::fusion::vector,和boost::tuple相当接近:
#include <string>
#include <cassert>
#include <boost/tuple/tuple.hpp>
#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/include/at_c.hpp>
void tuple_example() {
boost::tuple<int, int, std::string> tup(1, 2, "Meow");
assert(boost::get<0>(tup) == 1);
assert(boost::get<2>(tup) == "Meow");
}
void fusion_tuple_example() {
boost::fusion::vector<int, int, std::string> tup(1, 2, "Meow");
assert(boost::fusion::at_c<0>(tup) == 1);
assert(boost::fusion::at_c<2>(tup) == "Meow");
}但是boost::fusion::vector并不像boost::tuple那么简单。我们将在分裂元组配方中看到差异。
boost::fusion::for_each和std::for_each有一个根本区别。std::for_each函数内部包含一个循环,并在运行时决定必须进行多少次迭代。然而,boost::fusion::for_each()在编译时知道迭代次数并完全展开循环。对于boost::tuple<cat, int, std::string> tup2来说,boost::fusion::for_each(tup2, functor)呼叫相当于以下代码:
functor(boost::fusion::at_c<0>(tup2));
functor(boost::fusion::at_c<1>(tup2));
functor(boost::fusion::at_c<2>(tup2));C++ 11 不包含Boost.Fusion类。Boost.Fusion的所有方法都很有效。他们在编译时尽可能地做,并且有一些非常高级的优化。
C++ 14 增加了std::integer_sequence和std::make_integer_sequence来简化变量模板。使用这些实体,可以手动编写boost::fusion::for_each功能并实现stringize功能,而无需Boost.Fusion:
#include <utility>
#include <tuple>
template <class Tuple, class Func, std::size_t... I>
void stringize_cpp11_impl(const Tuple& t, const Func& f, std::index_sequence<I...>) {
// Oops. Requires C++ 17 fold expressions feature.
// (f(std::get<I>(t)), ...);
int tmp[] = { 0, (f(std::get<I>(t)), 0)... };
(void)tmp; // Suppressing unused variable warnings.
}
template <class Tuple>
std::string stringize_cpp11(const Tuple& t) {
std::string result;
stringize_cpp11_impl(
t,
stringize_functor(result),
std::make_index_sequence< std::tuple_size<Tuple>::value >()
);
return result;
}正如您可能看到的那样,许多代码都是为此而编写的,这样的代码并不容易阅读和理解。
C++ 标准化工作组讨论了在 C++ 20 标准中增加类似于constexpr for的内容的想法。有了这个特性,有一天我们可以编写以下代码(语法可能会改变!):
template <class Tuple>
std::string stringize_cpp20(const Tuple& t) {
std::string result;
for constexpr(const auto& v: t) {
result += boost::lexical_cast<std::string>(v);
}
return result;
}在此之前,Boost.Fusion似乎是最便携、最简单的解决方案。
- 分裂元组配方将给出更多关于
Boost.Fusion真实力量的信息 Boost.Fusion的官方文档包含一些有趣的例子和完整的参考资料,可以在http://boost.org/libs/fusion找到
这个食谱将展示Boost.Fusion库的一小部分能力。我们将把一个元组分成两个元组,一个包含算术类型,另一个包含所有其他类型。
这个食谱需要了解Boost.MPL、占位符和Boost.Tuple。建议从头开始阅读这一章。
这可能是本章最难的食谱之一。结果类型在编译时确定,这些类型的值在运行时填充:
- 为了实现这种混合,我们需要以下头:
#include <boost/fusion/include/remove_if.hpp>
#include <boost/type_traits/is_arithmetic.hpp>- 现在,我们准备创建一个返回非算术类型的函数:
template <class Sequence>
typename boost::fusion::result_of::remove_if<
const Sequence,
boost::is_arithmetic<boost::mpl::_1>
>::type get_nonarithmetics(const Sequence& seq)
{
return boost::fusion::remove_if<
boost::is_arithmetic<boost::mpl::_1>
>(seq);
}- 和一个返回算术类型的函数:
template <class Sequence>
typename boost::fusion::result_of::remove_if<
const Sequence,
boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> >
>::type get_arithmetics(const Sequence& seq)
{
return boost::fusion::remove_if<
boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> >
>(seq);
}就这样!现在,我们能够完成以下任务:
#include <boost/fusion/include/vector.hpp>
#include <cassert>
#include <boost/fusion/include/at_c.hpp>
#include <boost/blank.hpp>
int main() {
typedef boost::fusion::vector<
int, boost::blank, boost::blank, float
> tup1_t;
tup1_t tup1(8, boost::blank(), boost::blank(), 0.0);
boost::fusion::vector<boost::blank, boost::blank> res_na
= get_nonarithmetics(tup1);
boost::fusion::vector<int, float> res_a = get_arithmetics(tup1);
assert(boost::fusion::at_c<0>(res_a) == 8);
}Boost.Fusion背后的思想是编译器在编译时知道结构布局,无论编译器在编译时知道什么,我们都可能同时改变。Boost.Fusion允许我们修改不同的序列,添加和删除字段,以及更改字段类型。这就是我们在第二步和第三步中所做的;我们从元组中移除了非必填字段。
现在,让我们仔细看看get_nonarithmetics。首先,它的结果类型是使用以下结构推导出来的:
typename boost::fusion::result_of::remove_if<
const Sequence,
boost::is_arithmetic<boost::mpl::_1>
>::type这一定是我们熟悉的。在本章的【编译时获取函数结果类型】配方中,我们看到了类似这样的内容。Boost.MPL的占位符boost::mpl::_1与返回新序列类型的boost::fusion::result_of::remove_if元功能配合良好。
现在,让我们进入函数内部,观察下面的代码:
return boost::fusion::remove_if<
boost::is_arithmetic<boost::mpl::_1>
>(seq);请记住,编译器在编译时知道seq的所有类型。这意味着Boost.Fusion可以为seq的不同元素应用元功能,并为它们获取元功能结果。这也意味着Boost.Fusion知道如何将必填字段从旧结构复制到新结构。
However, Boost.Fusion tries not to copy fields as long as possible.
步骤 3 中的代码与步骤 2 中的代码非常相似,但是它有一个用于移除非必需类型的否定谓词。
我们的功能可以用于Boost.Fusion支持的任何类型,而不仅仅是boost::fusion::vector。
您可以对Boost.Fusion容器使用Boost.MPL功能。你只需要包括#include <boost/fusion/include/mpl.hpp>:
#include <boost/fusion/include/mpl.hpp>
#include <boost/mpl/transform.hpp>
#include <boost/type_traits/remove_const.hpp>
template <class Sequence>
struct make_nonconst: boost::mpl::transform<
Sequence,
boost::remove_const<boost::mpl::_1>
> {};
typedef boost::fusion::vector<
const int, const boost::blank, boost::blank
> type1;
typedef make_nonconst<type1>::type nc_type;
BOOST_STATIC_ASSERT((boost::is_same<
boost::fusion::result_of::value_at_c<nc_type, 0>::type,
int
>::value));
BOOST_STATIC_ASSERT((boost::is_same<
boost::fusion::result_of::value_at_c<nc_type, 1>::type,
boost::blank
>::value));
BOOST_STATIC_ASSERT((boost::is_same<
boost::fusion::result_of::value_at_c<nc_type, 2>::type,
boost::blank
>::value));We used boost::fusion::result_of::value_at_c instead of boost::fusion::result_of::at_c because boost::fusion::result_of::at_c returns the exact return type of the boost::fusion::at_c call, which is a reference. boost::fusion::result_of::value_at_c returns type without a reference.
Boost.Fusion和Boost.MPL库不是 C++ 17 的一部分。Boost.Fusion速度极快。它有很多优化。
值得一提的是,我们只看到了Boost.Fusion能力的极小一部分。关于它可以单独写一本书。
Boost.Fusion的良好教程和完整文档可在http://boost.org/libs/fusion获得- 您也可能希望在 http://boost.org/libs/mpl 看到
Boost.MPL的官方文档
我们在本章中看到的大多数元编程技巧早在 C++ 11 之前就已经发明了。可能,你已经听说过一些。
全新的怎么样?用一个把元编程颠倒过来并让你的眉毛上扬的库来实现 C++ 14 中以前的食谱怎么样?系好安全带,我们正在潜入Boost.Hana的世界。
这个食谱需要 C++ 11 和 C++ 14 的知识,尤其是 lambdas。您将需要一个真正的 C++ 14 兼容编译器来编译这个例子。
现在,让我们用Boost.Hana的方式来做一切:
- 从包含标题开始:
#include <boost/hana/traits.hpp>- 我们创建一个
is_arithmetic_功能对象:
constexpr auto is_arithmetic_ = [](const auto& v) {
auto type = boost::hana::typeid_(v);
return boost::hana::traits::is_arithmetic(type);
};- 现在,我们实现
get_nonarithmetics功能:
#include <boost/hana/remove_if.hpp>
template <class Sequence>
auto get_nonarithmetics(const Sequence& seq) {
return boost::hana::remove_if(seq, [](const auto& v) {
return is_arithmetic_(v);
});
}- 让我们反过来定义
get_arithmetics。只是为了好玩!
#include <boost/hana/filter.hpp>
constexpr auto get_arithmetics = [](const auto& seq) {
return boost::hana::filter(seq, is_arithmetic_);
};就这样。现在,我们可以使用这些功能:
#include <boost/hana/tuple.hpp>
#include <boost/hana/integral_constant.hpp>
#include <boost/hana/equal.hpp>
#include <cassert>
struct foo {
bool operator==(const foo&) const { return true; }
bool operator!=(const foo&) const { return false; }
};
int main() {
const auto tup1
= boost::hana::make_tuple(8, foo{}, foo{}, 0.0);
const auto res_na = get_nonarithmetics(tup1);
const auto res_a = get_arithmetics(tup1);
using boost::hana::literals::operator ""_c;
assert(res_a[0_c] == 8);
const auto res_na_expected = boost::hana::make_tuple(foo(), foo());
assert(res_na == res_na_expected);
}乍一看,代码似乎很简单,但事实并非如此。Boost.Hana把元编程反过来了!在前面的食谱中,我们直接使用类型,但是Boost.Hana创建了一个保存类型的变量,并且大多数时候使用变量。
看看第二步中的typeid_呼叫:
auto type = boost::hana::typeid_(v);它实际上返回一个变量。关于类型的信息现在隐藏在type变量中,可以通过调用decltype(type)::type来提取。
但是让我们一行一行地移动。在*步骤 2 中,*我们将通用λ存储到is_arithmetic_变量中。从这一点来看,我们可以将该变量用作功能对象。在 lambda 中,我们创建了一个type变量,它现在保存了关于v类型的信息。下一行是围绕std::is_arithmetic的特殊包装,它从type变量中提取关于v类型的信息,并将其传递给std::is_arithmetic特征。调用的结果是一个布尔积分常数。
现在,神奇的部分!存储在is_arithmetic_变量中的 Lambda 实际上从未被boost::hana::remove_if和boost::hana::filter函数调用过。所有使用它的Boost.Hana函数只需要 lambda 函数的结果类型,而不需要它的主体。我们可以放心地更改定义,整个示例将继续运行良好:
constexpr auto is_arithmetic_ = [] (const auto& v) {
assert(false);
auto type = boost::hana::typeid_(v);
return boost::hana::traits::is_arithmetic(type);
};在步骤 3 和 *4 中,我们分别调用boost::hana::remove_if和boost::hana::filter函数。在步骤 3 中,我们在λ内部使用了is_arithmetic_。在第四步,*我们直接用了。你可以使用任何你喜欢的语法,这只是习惯问题。
最后在main()中,我们检查一切都如预期的那样工作,并且索引 0 所代表的元组中的元素等于8:
using boost::hana::literals::operator ""_c;
assert(res_a[0_c] == 8);The best way to understand the Boost.Hana library is to experiment with it. You can do it online at http://apolukhin.github.io/Boost-Cookbook/.
有一个小细节没有描述。operator[]的元组访问是如何工作的?不可能有一个函数返回不同的类型!
如果你第一次遇到这个把戏,这很有趣。Boost.Hana的operator ""_c处理文字,并根据文字构造不同的类型:
- 如果写
0_c,则返回integral_constant<long long, 0> - 如果写
1_c,则返回integral_constant<long long, 1> - 如果写
2_c,则返回integral_constant<long long, 2>
boost::hana::tuple类实际上有很多operator[]重载,接受不同类型的integral_constant。根据整型常量的值,返回正确的元组元素。例如,如果你写some_tuple[1_c],那么tuple::operator[](integral_constant<long long, 1>)被索引称为元素1被返回。
Boost.Hana不是 C++ 17 的一部分。然而,该库的作者参加了 C++ 标准化会议,并提出了不同的有趣的东西来纳入 C++ 标准。
如果你期望从Boost.Hana比从Boost.MPL获得数量级更好的编译时间,那么不要。目前,编译器没有很好地处理Boost.Hana方法。这可能有一天会改变。
It's worth looking at the source codes of the Boost.Hana library to discover new interesting ways of using C++ 14 features. All the Boost libraries could be found at GitHub https://github.com/boostorg.
官方文档有更多的例子,一个完整的参考部分,一些更多的教程,和一个编译时性能部分。在 http://boost.org/libs/hana.享受`Boost.Hana`图书馆

