快速入门

优质
小牛编辑
140浏览
2023-12-01

本节的目的是从非常高的层次快速介绍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::tuplestd::make_tuple了。Hana也提供了自己的tuplemake_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>),"");

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

除了用于异构和编译时序列外,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::anyswitch语句。给定一个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::pairhana::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。为此,我们使用Hanafind_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_必须存在。下一步该处理非defaultcase了,我们这里用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接受一个元组和一个函数,并以元组的内容作为参数调用函数。解包的结果是调用该函数的结果。此例,函数是一个泛型lambdalambda调用了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_iffilterunpack)和异构容器(tuple,optional),放心,还有更多!本教程的后续部分将以友好的方式逐步介绍与Hana有关的概念。如果你想立即着手编写代码,可以用以下备忘表作为快速参考。这个备忘表囊括了最常用的算法和容器,还提供了简短的说明。