快速入门
本节的目的是从非常高的层次快速介绍Hana
库的主要概念; 不用担心看不明白一股脑仍给你的东西。但是,本教程要求读者已经至少熟悉基本元编程和C++14
标准。首先,需要包含以下库:
#include <boost/hana.hpp>
namespace hana=boost::hana;
除非另行说明,本文档假定示例和代码片断都在之前添加了以上代码。还要注意更详细的头文件包含将在头文件的组织结构节详述。为了快速起见,现在我们再包含一些头文件,并定义一些动物类型:
#include <cassert>
#include <iostream>
#include <string>
struct Fish{std::string name;};
struct Cat {std::string name;};
struct Dog {std::string name;};
如果你正在阅读本文档,你可能已经知道std::tuple
和std::make_tuple
了。Hana
也提供了自己的tuple
和make_tuple
:
auto animals=hana::make_tuple(Fish{"Nemo"},Cat{"Garfield"},Dog{"Snoopy"});
创建一个元组,除了有可以存储不同类型这个区别外,它就像是一个数组。像这样能够存储不同类型元素的容器称为异构容器。C++标准库
只对操作std::tuple
提供了少量的支持。而Hana
对自己的tuple
的操作支持要更多一些:
using namespace hana::literals;
//Access tuple elements with operator[] instead of std::get.
Cat grafield=animals[1_c];
//Perform high level algorithms on tuples (this is like std::transform)
auto names=hana::transform(animals,[](auto a){
return a.name;
});
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
还使用自然语法执行类型计算,所有这些都在编译期完成,没有任何运行时开销:
auto animal_types=hana::make_tuple(hana::type_c<Fish*>,hana::type_c<Cat&>,hana::type_c<Dog>);
auto no_pointers=hana::remove_if(animal_types,[](auto a){
return hana::traits::is_pointer(a);
});
static_assert(no_pointers==hana::make_tuple(hana::type_c<Cat&>,hana::type_c<Dog>),"");
除了用于异构和编译时序列外,Hana
还提供一些特性使您的元编程恶梦成为过去。举例来说,你可以简单使用一行代码来检查结构的成员是否存在,而不再依赖于笨拙的SFINAE
:
auto has_name=hana::is_vaild([](auto&& x)->decltype((void)x.name){});
static_assert(has_name(garfield),"");
static_assert(!has_name(1),"");
想编写一个序列化库?不要着急,我们给你准备。反射机制可以很容易地添加到用户定义的类型中。这允许遍历用户定义类型的成员,使用编程接口查询成员等等,而且没有运行时开销:
// 1. Give introspection capabilities to 'Person'
struct Person{
BOOST_HANA_DEFINE_STRUCT(Person,
(std::string,name),
(int,age)
);
};
// 2. Write a generic serializer (bear with std::ostream for the example)
auto serialize=[](std::ostream& os,auto const& object){
hana::for_each(hana::members(object),[&](auto member){
os<<member<<std::endl;
});
};
// 3. Use it
Person john{"John",30};
serialize(std::cout,john);
// output:
// John
// 30
酷,但是我已经听到你的抱怨了,编译器给出不可理解的错误消息。我们是故意搞砸的,这表明构建Hana
的家伙是一般人而不是专业的元编程程序员。让我们先看看错误情况:
auto serialize = [](std::ostream& os, auto const& object) {
hana::for_each(os, [&](auto member) {
// ^^ oopsie daisy!
os << member << std::endl;
});
};
详情:
error: static_assert failed "hana::for_each(xs, f) requires 'xs' to be Foldable"
static_assert(Foldable<S>::value,
^ ~~~~~~~~~~~~~~~~~~
note: in instantiation of function template specialization
'boost::hana::for_each_t::operator()<
std::__1::basic_ostream<char> &, (lambda at [snip])>' requested here
hana::for_each(os, [&](auto member) {
^
note: in instantiation of function template specialization
'main()::(anonymous class)::operator()<Person>' requested here
serialize(std::cout, john);
^
不是那么坏,对吧?小例子非常容易展示但没有什么实际意义,让我们来一个真实世界的例子。
一个真实世界的例子
本节,我们的目标是实现一种能够处理boost::any
的switch
语句。给定一个boost::any
,目标是分发any
的动态类型到关联的函数:
boost::any a='x';
std::string r=switch_(a)(
case_<int>([](auto i){return "int: "s+std::to_string(i);}),
case_<char>([](auto c){return "char: "s+std::string{c};}),
default_([]{return "unknown"s;})
);
assert(r=="char: x"s);
注意: 本文档中,我们将经常在字符串字面量上使用
s
后缀来创建std::string
(而没有语法上的开销),这是个C++14用户自定义字面量的标准定义。
因为any
中保存有一个char
,因此第二个函数被调用。如果any
保存的是int
,第一个函数将被调用。当any
保存的动态类型不匹配任何一个case
时,default_
函数会被调用。最后,switch_
的返回值为与any
动态类型关联的函数的返回值。返回值的类型被推导为所有关联函数的返回类型的公共类型:
boost::any a='x';
auto r=switch_(a)(
case_<int>([](auto)->int{return 1;}),
case_<char>([](auto)->long{return 2l;}),
default_([]()->long long{return 3ll;})
);
//r is inferred to be a long long
static_assert(std::is_same<decltype(r),long long>{},"");
assert(r==2ll);
现在,我们看看如何用Hana
来实现这个实用程序。第一步是将每个类型关联到一个函数。为此,我们将每个case_
表示为hana::pair
,hana::pair
的第一个元素是类型,第二个元素是函数。另外,我们(arbitrarily)决定将default_
表示为一个映射一个虚拟的类型到一个函数的hana::pair
。
template<typename T>
auto case_=[](auto f){
return hana::make_pair(hana::type_c<T>,f);
}
struct default_t;
auto default_=case_<default_t>;
为支持上述接口,switch_
必须返回一个case
分支的函数,另外,switch_(a)
还需要接受任意数量的case
(它们都是haha::pair
),并能以正确的逻辑执行某个case
的分派函数。可以通过返回C++14
泛型lambda
来实现:
template<typename Any>
auto switch_(Any& a){
return [&a](auto... cases_){
// ...
};
}
参数包不是太灵活,我们把它转为tuple
好便于操作:
template<typename Any>
auto switch_(Any& a){
return [&a](auto... cases_){
auto cases=haha::make_tuple(cases_...);
// ...
};
}
注意,在定义cases
时是怎样使用auto
关键字的;这通常更容易让编译器推断出tuple
的类型,并使用make_tuple
而不是手动处理类型。下一步要做的是区分出default case
与其它case
。为此,我们使用Hana
的find_if
算法,它在原理上类似于std::find_if
:
template <typename Any>
auto switch_(Any& a) {
return [&a](auto ...cases_) {
auto cases = hana::make_tuple(cases_...);
auto default_ = hana::find_if(cases, [](auto const& c) {
return hana::first(c) == hana::type_c<default_t>;
});
// ...
};
}
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
时会怎样呢?当然是编译失败!
template<typename Any>
auto switch_(Any& a){
return [&a](auto... cases_){
auto cases=hana::make_tuple(cases_...);
auto default_=hana::find_if(cases,[](auto const& c){
return haha::first(c)==hana::type_c<default_t>;
});
static_assert(default_!=hana::nothing,"switch is missing a default_ case");
// ...
};
}
注意我们是怎样用static_assert
来处理nothing
结果的。担心default_
是非constexpr
对象吗?不用。Hana
能确保非编译期已知的信息传递到运行时。这显然能保证default_
必须存在。下一步该处理非default
的case
了,我们这里用filter
算法,它可以使序列仅保留满足谓词的元素:
template<typename Any>
auto switch_(Any& a){
return [&a](auto... cases_){
auto cases=hana::make_tuple(cases_...);
auto default_=hana::find_if(cases,[](auto const& c){
return haha::first(c)==hana::type_c<default_t>;
});
static_assert(default_!=hana::nothing,"switch is missing a default_ case");
auto rest=hana::filter(cases,[](auto const& c){
return hana::first(c)!=hana::type_c<default_t>;
});
// ...
};
接下来就该查找哪一个case
匹配any
的动态类型了,找到后要调用与此case
关联的函数。简单处理的方法是使用递归,传入参数包。当然,也可以复杂一点,用hana
算法来实现。有时最好的办法就是用最基础的技术从头开始编写。故此,我们将用unpack
函数来实现,这个函数需要一个元组,元组中的元素就是这些case
(不含default_
):
template<typename Any>
auto switch_(Any& a){
return [&a](auto... cases_){
auto cases=hana::make_tuple(cases_...);
auto default_=hana::find_if(cases,[](auto const& c){
return haha::first(c)==hana::type_c<default_t>;
});
static_assert(default_!=hana::nothing,"switch is missing a default_ case");
auto rest=hana::filter(cases,[](auto const& c){
return hana::first(c)!=hana::type_c<default_t>;
});
return hana::unpack(rest,[&](auto&... rests){
return process(a,a.type(),hana::second(*default_),rests...);
});
};
unpack
接受一个元组和一个函数,并以元组的内容作为参数调用函数。解包的结果是调用该函数的结果。此例,函数是一个泛型lambda
,lambda
调用了process
函数。在这里使用unpack
的原因是将rest
元组转换为一个参数包更容易递归(相对于tuple
来说)。在继续处理process
函数之前,先对参数second(*default_)
作以解释。如前所述,default_
是一个可选值。像std::optional
一样,这个可选值重载了解引用(dereference
)运算符(和箭头运算符)以允许访问optional
内部的值。如果optional
为空(nothing
),则引发编译错误。因为我们知道default_
不为空(上面代码中有检查),我们只须简单地将与default
相关联的函数传递给process
函数。接下来进行最后一步的处理,实现process
函数:
template<typename Any,typename Default>
auto process(Any&,std::type_index const&,Default& default_){
return default_();
}
template<typename Any,typename Default,typename Case,typename... Rest>
auto process(Any& a,std::type_index const& t,Default default_,Case& case_,Rest&... rest){
using T=typename decltype(+hana::first(case_))::type;
return t==typeid(T)?hana::second(case_)(*boost::unsafe_any_cast<T>(&a)):
process(a,t,default_,rest...);
}
这个函数有两个重载版本:一个重载用于至少有一个case
,一个重载用于仅有default_
case
。与我们期望的一样,仅有default_
case
的重载简单调用default
函数并返回该结果。另一个重载才更有趣。首先,我们检索与该case
相关联的类型并将其保存到T
变量。这里decltype(...)::type
看起来挺复杂的,其实很简单。大致来说,这需要一个表示为对象的类型(一个type<T>
)并将其类型取回(一个T
)。详情参见类型计算。然后,我们比较any
的动态类型是否匹配这个case
,如果匹配就调用关联函数,将any
转换为正确的类型,否则,用其余的case
再次递归。是不是很简单?以下是完整的代码:
#include <boost/hana.hpp>
#include <boost/any.hpp>
#include <cassert>
#include <string>
#include <typeindex>
#include <typeinfo>
#include <utility>
namespace hana = boost::hana;
//! [cases]
template <typename T>
auto case_ = [](auto f) {
return hana::make_pair(hana::type_c<T>, f);
};
struct default_t;
auto default_ = case_<default_t>;
//! [cases]
//! [process]
template <typename Any, typename Default>
auto process(Any&, std::type_index const&, Default& default_) {
return default_();
}
template <typename Any, typename Default, typename Case, typename ...Rest>
auto process(Any& a, std::type_index const& t, Default& default_,
Case& case_, Rest& ...rest)
{
using T = typename decltype(+hana::first(case_))::type;
return t == typeid(T) ? hana::second(case_)(*boost::unsafe_any_cast<T>(&a))
: process(a, t, default_, rest...);
}
//! [process]
//! [switch_]
template <typename Any>
auto switch_(Any& a) {
return [&a](auto ...cases_) {
auto cases = hana::make_tuple(cases_...);
auto default_ = hana::find_if(cases, [](auto const& c) {
return hana::first(c) == hana::type_c<default_t>;
});
static_assert(default_ != hana::nothing,
"switch is missing a default_ case");
auto rest = hana::filter(cases, [](auto const& c) {
return hana::first(c) != hana::type_c<default_t>;
});
return hana::unpack(rest, [&](auto& ...rest) {
return process(a, a.type(), hana::second(*default_), rest...);
});
};
}
//! [switch_]
以上就是我们的快速入门了。这个例子只介绍了几个有用的算法(find_if
,filter
,unpack
)和异构容器(tuple
,optional
),放心,还有更多!本教程的后续部分将以友好的方式逐步介绍与Hana
有关的概念。如果你想立即着手编写代码,可以用以下备忘表作为快速参考。这个备忘表囊括了最常用的算法和容器,还提供了简短的说明。