快速入门

本节的目的是从非常高的层次快速介绍Hana库的主要概念; 不用担心看不明白一股脑仍给你的东西。但是,本教程要求读者已经至少熟悉基本元编程和C++14标准。首先,需要包含以下库:

  1. #include <boost/hana.hpp>
  2. namespace hana=boost::hana;

除非另行说明,本文档假定示例和代码片断都在之前添加了以上代码。还要注意更详细的头文件包含将在头文件的组织结构节详述。为了快速起见,现在我们再包含一些头文件,并定义一些动物类型:

  1. #include <cassert>
  2. #include <iostream>
  3. #include <string>
  4. struct Fish{std::string name;};
  5. struct Cat {std::string name;};
  6. struct Dog {std::string name;};

如果你正在阅读本文档,你可能已经知道std::tuplestd::make_tuple了。Hana也提供了自己的tuplemake_tuple:

  1. auto animals=hana::make_tuple(Fish{"Nemo"},Cat{"Garfield"},Dog{"Snoopy"});

创建一个元组,除了有可以存储不同类型这个区别外,它就像是一个数组。像这样能够存储不同类型元素的容器称为异构容器。C++标准库只对操作std::tuple提供了少量的支持。而Hana对自己的tuple的操作支持要更多一些:

  1. using namespace hana::literals;
  2. //Access tuple elements with operator[] instead of std::get.
  3. Cat grafield=animals[1_c];
  4. //Perform high level algorithms on tuples (this is like std::transform)
  5. auto names=hana::transform(animals,[](auto a){
  6. return a.name;
  7. });
  8. assert(hana::reverse(names)==hana::make_tuple("Snoopy","Garfield","Nemo"));

注意: 1_c是一个用C++14用户自定义字面量创建的编译期数值。此自定义字面量位于boost::hana::literals名字空间,故此using了该名字空间。

注意我们是如何将C++14泛型lambda传递到transform的;必须要这样做是因为lambda首先用Fish来调用的,接着用Cat,最后用Dog来调用,它们都是类型不同的。Hana提供了C++标准提供的大多数算法,除了它们工作在元组和异构容器上而不是在std::tuple等之上的之外。除了使用异构值之外,Hana还使用自然语法执行类型计算,所有这些都在编译期完成,没有任何运行时开销:

  1. auto animal_types=hana::make_tuple(hana::type_c<Fish*>,hana::type_c<Cat&>,hana::type_c<Dog>);
  2. auto no_pointers=hana::remove_if(animal_types,[](auto a){
  3. return hana::traits::is_pointer(a);
  4. });
  5. static_assert(no_pointers==hana::make_tuple(hana::type_c<Cat&>,hana::type_c<Dog>),"");

注意: type_c<...>不是一个类型!它是一个C++14变量模板生成的Hana类型对象。更多详情参见类型计算

除了用于异构和编译时序列外,Hana还提供一些特性使您的元编程恶梦成为过去。举例来说,你可以简单使用一行代码来检查结构的成员是否存在,而不再依赖于笨拙的SFINAE

  1. auto has_name=hana::is_vaild([](auto&& x)->decltype((void)x.name){});
  2. static_assert(has_name(garfield),"");
  3. static_assert(!has_name(1),"");

想编写一个序列化库?不要着急,我们给你准备。反射机制可以很容易地添加到用户定义的类型中。这允许遍历用户定义类型的成员,使用编程接口查询成员等等,而且没有运行时开销:

  1. // 1. Give introspection capabilities to 'Person'
  2. struct Person{
  3. BOOST_HANA_DEFINE_STRUCT(Person,
  4. (std::string,name),
  5. (int,age)
  6. );
  7. };
  8. // 2. Write a generic serializer (bear with std::ostream for the example)
  9. auto serialize=[](std::ostream& os,auto const& object){
  10. hana::for_each(hana::members(object),[&](auto member){
  11. os<<member<<std::endl;
  12. });
  13. };
  14. // 3. Use it
  15. Person john{"John",30};
  16. serialize(std::cout,john);
  17. // output:
  18. // John
  19. // 30

酷,但是我已经听到你的抱怨了,编译器给出不可理解的错误消息。我们是故意搞砸的,这表明构建Hana的家伙是一般人而不是专业的元编程程序员。让我们先看看错误情况:

  1. auto serialize = [](std::ostream& os, auto const& object) {
  2. hana::for_each(os, [&](auto member) {
  3. // ^^ oopsie daisy!
  4. os << member << std::endl;
  5. });
  6. };

详情:

  1. error: static_assert failed "hana::for_each(xs, f) requires 'xs' to be Foldable"
  2. static_assert(Foldable<S>::value,
  3. ^ ~~~~~~~~~~~~~~~~~~
  4. note: in instantiation of function template specialization
  5. 'boost::hana::for_each_t::operator()<
  6. std::__1::basic_ostream<char> &, (lambda at [snip])>' requested here
  7. hana::for_each(os, [&](auto member) {
  8. ^
  9. note: in instantiation of function template specialization
  10. 'main()::(anonymous class)::operator()<Person>' requested here
  11. serialize(std::cout, john);
  12. ^

不是那么坏,对吧?小例子非常容易展示但没有什么实际意义,让我们来一个真实世界的例子。

一个真实世界的例子

本节,我们的目标是实现一种能够处理boost::anyswitch语句。给定一个boost::any,目标是分发any的动态类型到关联的函数:

  1. boost::any a='x';
  2. std::string r=switch_(a)(
  3. case_<int>([](auto i){return "int: "s+std::to_string(i);}),
  4. case_<char>([](auto c){return "char: "s+std::string{c};}),
  5. default_([]{return "unknown"s;})
  6. );
  7. assert(r=="char: x"s);

注意: 本文档中,我们将经常在字符串字面量上使用s后缀来创建std::string(而没有语法上的开销),这是个C++14用户自定义字面量的标准定义。

因为any中保存有一个char,因此第二个函数被调用。如果any保存的是int,第一个函数将被调用。当any保存的动态类型不匹配任何一个case时,default_函数会被调用。最后,switch_的返回值为与any动态类型关联的函数的返回值。返回值的类型被推导为所有关联函数的返回类型的公共类型:

  1. boost::any a='x';
  2. auto r=switch_(a)(
  3. case_<int>([](auto)->int{return 1;}),
  4. case_<char>([](auto)->long{return 2l;}),
  5. default_([]()->long long{return 3ll;})
  6. );
  7. //r is inferred to be a long long
  8. static_assert(std::is_same<decltype(r),long long>{},"");
  9. assert(r==2ll);

现在,我们看看如何用Hana来实现这个实用程序。第一步是将每个类型关联到一个函数。为此,我们将每个case_表示为hana::pairhana::pair的第一个元素是类型,第二个元素是函数。另外,我们(arbitrarily)决定将default_表示为一个映射一个虚拟的类型到一个函数的hana::pair

  1. template<typename T>
  2. auto case_=[](auto f){
  3. return hana::make_pair(hana::type_c<T>,f);
  4. }
  5. struct default_t;
  6. auto default_=case_<default_t>;

为支持上述接口,switch_必须返回一个case分支的函数,另外,switch_(a)还需要接受任意数量的case(它们都是haha::pair),并能以正确的逻辑执行某个case的分派函数。可以通过返回C++14泛型lambda来实现:

  1. template<typename Any>
  2. auto switch_(Any& a){
  3. return [&a](auto... cases_){
  4. // ...
  5. };
  6. }

参数包不是太灵活,我们把它转为tuple好便于操作:

  1. template<typename Any>
  2. auto switch_(Any& a){
  3. return [&a](auto... cases_){
  4. auto cases=haha::make_tuple(cases_...);
  5. // ...
  6. };
  7. }

注意,在定义cases时是怎样使用auto关键字的;这通常更容易让编译器推断出tuple的类型,并使用make_tuple而不是手动处理类型。下一步要做的是区分出default case与其它case。为此,我们使用Hanafind_if算法,它在原理上类似于std::find_if

  1. template <typename Any>
  2. auto switch_(Any& a) {
  3. return [&a](auto ...cases_) {
  4. auto cases = hana::make_tuple(cases_...);
  5. auto default_ = hana::find_if(cases, [](auto const& c) {
  6. return hana::first(c) == hana::type_c<default_t>;
  7. });
  8. // ...
  9. };
  10. }

find_if接受一个元组和一个谓词,返回元组中满足谓词条件的第一个元素。返回结果是一个hana::optional,它类似于std::optional,除了可选值为empty或不是编译时已知的。如果元组的元素不满足谓词条件,find_if不返回任何值(空值)。否则,返回just(x)(非空值),其中x是满足谓词的第一个元素。与STL算法中使用的谓词不同,此处使用的谓词必须是泛型的,因为元组中的元素是异构的。此外,该谓词必须返回Hana可调用的IntegeralConstant,这意味着谓词的结果必须是编译时已知的。更多细节请参见交叉相位算法算法)。在谓词内部,我们只需将cases的第一个元素的类型与type_c<default_t>比较。如果还记得我们使用hana::pair来对case进行编码的话,这里的意思即为我们在所有提供的case中找到default case。但是,如果没有提供default case时会怎样呢?当然是编译失败!

  1. template<typename Any>
  2. auto switch_(Any& a){
  3. return [&a](auto... cases_){
  4. auto cases=hana::make_tuple(cases_...);
  5. auto default_=hana::find_if(cases,[](auto const& c){
  6. return haha::first(c)==hana::type_c<default_t>;
  7. });
  8. static_assert(default_!=hana::nothing,"switch is missing a default_ case");
  9. // ...
  10. };
  11. }

注意我们是怎样用static_assert来处理nothing结果的。担心default_是非constexpr对象吗?不用。Hana能确保非编译期已知的信息传递到运行时。这显然能保证default_必须存在。下一步该处理非defaultcase了,我们这里用filter算法,它可以使序列仅保留满足谓词的元素:

  1. template<typename Any>
  2. auto switch_(Any& a){
  3. return [&a](auto... cases_){
  4. auto cases=hana::make_tuple(cases_...);
  5. auto default_=hana::find_if(cases,[](auto const& c){
  6. return haha::first(c)==hana::type_c<default_t>;
  7. });
  8. static_assert(default_!=hana::nothing,"switch is missing a default_ case");
  9. auto rest=hana::filter(cases,[](auto const& c){
  10. return hana::first(c)!=hana::type_c<default_t>;
  11. });
  12. // ...
  13. };

接下来就该查找哪一个case匹配any的动态类型了,找到后要调用与此case关联的函数。简单处理的方法是使用递归,传入参数包。当然,也可以复杂一点,用hana算法来实现。有时最好的办法就是用最基础的技术从头开始编写。故此,我们将用unpack函数来实现,这个函数需要一个元组,元组中的元素就是这些case(不含default_):

  1. template<typename Any>
  2. auto switch_(Any& a){
  3. return [&a](auto... cases_){
  4. auto cases=hana::make_tuple(cases_...);
  5. auto default_=hana::find_if(cases,[](auto const& c){
  6. return haha::first(c)==hana::type_c<default_t>;
  7. });
  8. static_assert(default_!=hana::nothing,"switch is missing a default_ case");
  9. auto rest=hana::filter(cases,[](auto const& c){
  10. return hana::first(c)!=hana::type_c<default_t>;
  11. });
  12. return hana::unpack(rest,[&](auto&... rests){
  13. return process(a,a.type(),hana::second(*default_),rests...);
  14. });
  15. };

unpack接受一个元组和一个函数,并以元组的内容作为参数调用函数。解包的结果是调用该函数的结果。此例,函数是一个泛型lambdalambda调用了process函数。在这里使用unpack的原因是将rest元组转换为一个参数包更容易递归(相对于tuple来说)。在继续处理process函数之前,先对参数second(*default_)作以解释。如前所述,default_是一个可选值。像std::optional一样,这个可选值重载了解引用(dereference)运算符(和箭头运算符)以允许访问optional内部的值。如果optional为空(nothing),则引发编译错误。因为我们知道default_不为空(上面代码中有检查),我们只须简单地将与default相关联的函数传递给process函数。接下来进行最后一步的处理,实现process函数:

  1. template<typename Any,typename Default>
  2. auto process(Any&,std::type_index const&,Default& default_){
  3. return default_();
  4. }
  5. template<typename Any,typename Default,typename Case,typename... Rest>
  6. auto process(Any& a,std::type_index const& t,Default default_,Case& case_,Rest&... rest){
  7. using T=typename decltype(+hana::first(case_))::type;
  8. return t==typeid(T)?hana::second(case_)(*boost::unsafe_any_cast<T>(&a)):
  9. process(a,t,default_,rest...);
  10. }

这个函数有两个重载版本:一个重载用于至少有一个case,一个重载用于仅有default_ case。与我们期望的一样,仅有default_ case的重载简单调用default函数并返回该结果。另一个重载才更有趣。首先,我们检索与该case相关联的类型并将其保存到T变量。这里decltype(...)::type看起来挺复杂的,其实很简单。大致来说,这需要一个表示为对象的类型(一个type<T>)并将其类型取回(一个T)。详情参见类型计算。然后,我们比较any的动态类型是否匹配这个case,如果匹配就调用关联函数,将any转换为正确的类型,否则,用其余的case再次递归。是不是很简单?以下是完整的代码:

  1. #include <boost/hana.hpp>
  2. #include <boost/any.hpp>
  3. #include <cassert>
  4. #include <string>
  5. #include <typeindex>
  6. #include <typeinfo>
  7. #include <utility>
  8. namespace hana = boost::hana;
  9. //! [cases]
  10. template <typename T>
  11. auto case_ = [](auto f) {
  12. return hana::make_pair(hana::type_c<T>, f);
  13. };
  14. struct default_t;
  15. auto default_ = case_<default_t>;
  16. //! [cases]
  17. //! [process]
  18. template <typename Any, typename Default>
  19. auto process(Any&, std::type_index const&, Default& default_) {
  20. return default_();
  21. }
  22. template <typename Any, typename Default, typename Case, typename ...Rest>
  23. auto process(Any& a, std::type_index const& t, Default& default_,
  24. Case& case_, Rest& ...rest)
  25. {
  26. using T = typename decltype(+hana::first(case_))::type;
  27. return t == typeid(T) ? hana::second(case_)(*boost::unsafe_any_cast<T>(&a))
  28. : process(a, t, default_, rest...);
  29. }
  30. //! [process]
  31. //! [switch_]
  32. template <typename Any>
  33. auto switch_(Any& a) {
  34. return [&a](auto ...cases_) {
  35. auto cases = hana::make_tuple(cases_...);
  36. auto default_ = hana::find_if(cases, [](auto const& c) {
  37. return hana::first(c) == hana::type_c<default_t>;
  38. });
  39. static_assert(default_ != hana::nothing,
  40. "switch is missing a default_ case");
  41. auto rest = hana::filter(cases, [](auto const& c) {
  42. return hana::first(c) != hana::type_c<default_t>;
  43. });
  44. return hana::unpack(rest, [&](auto& ...rest) {
  45. return process(a, a.type(), hana::second(*default_), rest...);
  46. });
  47. };
  48. }
  49. //! [switch_]

以上就是我们的快速入门了。这个例子只介绍了几个有用的算法(find_iffilterunpack)和异构容器(tuple,optional),放心,还有更多!本教程的后续部分将以友好的方式逐步介绍与Hana有关的概念。如果你想立即着手编写代码,可以用以下备忘表作为快速参考。这个备忘表囊括了最常用的算法和容器,还提供了简短的说明。