Hana内核
本节的目标是对Hana
的核心进行一个高层次的概述。 这个核心是基于tag
的概念,它是从Boost.Fusion
和Boost.MPL
库借用的,Hana
更进一步地加深了这个概念。 这些tag
随后会用于多个目的,如算法定制,文档分组,改进错误消息和将容器转换为其他容器等。 得益于其模块化设计,Hana
可以非常容易地以ad-hoc方式扩展。 事实上,库的所有功能都是通过ad-hoc定制机制提供的。
Tags
一般来说异构编程基本上就是使用具有不同类型的对象进行编程。然而,我们也清楚地看到,一些对象的家族,虽然具有不同的(C++类型
)表示,但它们是强相关的。例如,std::integral_constant<int,n>
类型对于每个不同的n
类型是不同的,但在概念上它们都代表相同的东西—编译时数值。事实上,std::integral_constant<int,1>{}
和std::integral_constant<int,2>{}
有不同的类型只是这个事实的副作用:我们使用他们的类型来编码这些对象。实际上,当操作std::integral_constant<int,...>
的序列时,你可能会认为它是一个虚拟的integral_constant
类型的同构序列,忽略对象的实际类型,假装它们只是使用了不同的integral_constant
值。
为了反映这种情况,Hana
提供了tag
来表示异构容器和其他编译时实体。例如,所有的Hana
的integral_constant<int,...>
都有不同的类型,但是它们都有相同的tag
,即integral_constant_tag<int>
。这就允许程序员根据单个类型来思考问题,而不是试图考虑每个对象的实际类型。具体来说,tag
被实现为空结构体。为了区别它们,Hana
约定通过添加_tag
后缀来命名这些tag
。
注意: 可以通过使用
tag_of<T>::type
或等效的tag_of_t<T>
来获得与T
的类型关联的tag
对象类型。
tag
是正常C++
类型的扩展。 事实上,默认情况下,类型T
的tag
是T
本身,库的核心就是设计为在这些情况下工作。 例如,hana::make
期望tag
或实际类型; 如果你传递一个类型T
,它会做逻辑的事情,并用你传递的参数构造一个类型T
的对象。 如果您向其传递tag
,则应该专门针对该tag
进行处理,并提供自己的实现,如下所述。 因为tag
是对正常类型的扩展,所以我们最终使用的是tag
类型而不是正常类型,文档表述中有时使用单词形式的类型,数据类型和标签可互换。
Tag分发
标签分发(或标签派发)是一种通用的编程技术,用于根据传递给函数的参数类型来选择函数的正确实现。 重写函数行为的通常机制是重载。不幸的是,当处理具有不同基本模板的相关类型的族时,或者当模板参数的种类不是已知的(是类型还是非类型模板参数?)时,这种机制并不总是方便的。 例如,考虑尝试为所有Boost.Fusion
vector
重载一个函数:
template <typename ...T>
void function(boost::fusion::vector<T...> v) {
// whatever
}
如果你知道Boost.Fusion
,那么你可能知道它不会工作。 这是因为Boost.Fusion
的vector
不一定是boost::fusion::vector
模板的特化。 Fusion
的vector
以编号形式存在,它们都是不同类型:
boost::fusion::vector1<T>
boost::fusion::vector2<T, U>
boost::fusion::vector3<T, U, V>
...
这是一个实现细节,因为C++03
(很不幸的)缺少可变参数模板,但我们需要一种方法来解决它。为此,我们使用具有三个不同组件的基础结构:
- 一个元函数将单个
tag
关联到相关类型系列中的每个类型。在Hana
中,可以使用tag_of
元函数访问此标记。具体来说,对于任何类型T
,tag_of<T>::type
是用于分派它的标签。- 属于库的公共接口的函数,我们希望能够提供自定义实现。在
Hana
中,这些函数是与concept
相关的算法,如transform
或unpack
。- 函数的实现,将参数化的标签传递给函数参数。在
Hana
中,这通常通过具有一个名为xxx_impl
(用于接口函数xxx
)的单独模板与嵌套应用静态函数来完成,如下所示。
当调用public
接口函数xxx
时,它将获得它希望分派调用的参数的标签,然后将调用转发到与这些标签相关联的xxx_impl
实现。例如,以下示例,print
函数根据参数类型的标签来选择对应的特化版本:
template <typename Tag>
struct print_impl {
template <typename X>
static void apply(std::ostream&, X const&) {
// possibly some default implementation
}
};
template <typename X>
void print(std::ostream& os, X x) {
using Tag = typename hana::tag_of<X>::type;
print_impl<Tag>::apply(os, x);
}
现在,让我们定义一个类型,需要标签分派来自定义输出的行为。 虽然有一些C++14
的例子,但它们太复杂,不能在本教程中展示,因此我们将使用一个C++03
元组实现为几种不同的类型来说明该技术:
struct vector_tag;
struct vector0 {
using hana_tag = vector_tag;
static constexpr std::size_t size = 0;
};
template <typename T1>
struct vector1 {
T1 t1;
using hana_tag = vector_tag;
static constexpr std::size_t size = 1;
template <typename Index>
auto const& operator[](Index i) const {
static_assert(i == 0u, "index out of bounds");
return t1;
}
};
template <typename T1, typename T2>
struct vector2 {
T1 t1; T2 t2;
using hana_tag = vector_tag;
static constexpr std::size_t size = 2;
// Using Hana as a backend to simplify the example.
template <typename Index>
auto const& operator[](Index i) const {
return *hana::make_tuple(&t1, &t2)[i];
}
};
// and so on...
嵌套类型using hana_tag = vector_tag
部分是控制tag_of
元函数的结果的简单方式,因此是vectorN
类型的标签。 参见tag_of
的解释。 最后,如果你想为所有的vectorN
类型定制输出函数的行为,你通常需要:
void print(std::ostream& os, vector0)
{ os << "[]"; }
template <typename T1>
void print(std::ostream& os, vector1<T1> v)
{ os << "[" << v.t1 << "]"; }
template <typename T1, typename T2>
void print(std::ostream& os, vector2<T1, T2> v)
{ os << "[" << v.t1 << ", " << v.t2 << "]"; }
// and so on...
现在,使用标签分派,您可以依赖于所有共享相同标签的vectorNs
,特化print_impl
结构:
template <>
struct print_impl<vector_tag> {
template <typename vectorN>
static void apply(std::ostream& os, vectorN xs) {
constexpr auto N = hana::size_c<vectorN::size>;
os << "[";
N.times.with_index([&](auto i) {
os << xs[i];
if (i != N - hana::size_c<1>) os << ", ";
});
os << "]";
}
};
一个优点是,所有的vectorNs
现在只需通过一个print
函数处理,代价是在创建数据结构(以指定每个向量N
的标签)和创建初始输出函数(设置标签调度系统)时,特化(print_impl
)。 这种技术还有其他优点,如在接口函数中检查前提条件的能力,而不必乏味地在每个自定义实现中执行:
template <typename X>
void print(std::ostream& os, X x) {
// **** check some precondition ****
// The precondition only has to be checked here; implementations
// can assume their arguments to always be sane.
using Tag = typename hana::tag_of<X>::type;
print_impl<Tag>::apply(os, x);
}
注意: 检查前提条件对于输出函数没有多大意义,但是例如考虑获得序列的第
n
个元素的函数; 您可能需要确保索引不超出界限。
这种技术还使得更容易提供接口函数作为函数对象而不是普通的重载函数,因为只有接口函数本身必须经历定义函数对象的麻烦。 函数对象具有比重载函数更多的优点,例如用于更高阶算法或变量的能力:
// Defining a function object is only needed once and implementations do not
// have to worry about static initialization and other painful tricks.
struct print_t {
template <typename X>
void operator()(std::ostream& os, X x) const {
using Tag = typename hana::tag_of<X>::type;
print_impl<Tag>::apply(os, x);
}
};
constexpr print_t print{};
你可能知道,能够同时为许多类型实现一个算法是非常有用的(这正是C++模板
的目标!)。然而,甚至更有用的是为满足一些条件的许多类型实现算法的能力。 C++模板
目前缺少这种限制模板参数的能力,但是一个称为Concept
的语言特性正在推出,目的是解决这个问题。
有了类似的想法,Hana
的算法支持一个额外的标签调度层,如上所述。这个层允许我们为所有类型满足一些谓词的算法“专门化”。例如,假设我们想对所有表示某种序列的类型实现上面的print
函数。现在,我们不会有一个简单的方法来做到这一点。然而,Hana
算法的标签调度设置与上面显示的略有不同,因此我们可以写下:
template <typename Tag>
struct print_impl<Tag, hana::when<Tag represents some kind of sequence>> {
template <typename Seq>
static void apply(std::ostream& os, Seq xs) {
// Some implementation for any sequence
}
};
其中Tag
表示某种类型的序列将仅需要表示Tag
是否是序列的布尔表达式。我们将看到如何在下一节中创建这样的谓词,但现在让我们假设它是可工作的。在不详细说明如何设置该标签分配的情况下,上述特化仅在满足该谓词,并且如果没有找到更好的匹配时被选取。因此,例如,如果我们的vector_tag
要满足谓词,我们对vector_tag
的初始实现仍然优先于基于hana::when
的特化,因为它表示更好的匹配。一般来说,任何不使用hana::when
的特化(无论是显式还是部分)将优先于使用hana::when
的特化,这从用户的角度来看可能不会令人惊讶。本节涵盖了几乎所有关于Hana
的标签调度。下一节将解释如何为元编程创建C++ Concept
,然后可以与hana::when
结合使用来实现更好的表现力。
模拟C++Concept
Hana
中concept
的实现非常简单。 在它的核心,一个concept
只是一个struct模板
继承自一个布尔的integral_constant
表示给定的类型是一个concept
的模塑:
template <typename T>
struct Concept
: hana::integral_constant<bool, whether T models Concept>
{ };
然后,可以通过查看Concept<T>::value
来测试类型T
是否模塑了Concept
。很简单,对吧?现在,虽然可能实现检查的方式不一定是任何具体的HANA
,本节的其余部分将解释Hana
是怎样做的,以及它如何与标签调度交互。然后,您应该能够定义自己的concept
,如果你愿意,或至少更好地了解Hana
内部工作。
通常,Hana
定义的concept
将要求任何模型实现一些标签分派的函数。例如,Foldable
概念要求任何模型定义至少一个hana::unpack
和hana::fold_left
。当然,concept
通常也定义语义要求(称为法则),它们必须由他们的模型满足,但是这些规律不是(也不能)被concept
检查。但是我们如何检查一些功能是否正确实现了?为此,我们必须稍微修改我们定义的标签调度方法,如上一节所示。让我们回到我们的print
示例,并尝试为可打印的对象定义一个Printable
概念。我们的最终目标是拥有一个模板结构:
template <typename T>
struct Printable
: hana::integral_constant<bool, whether print_impl<tag of T> is defined>
{ };
要知道是否定义了print_impl<...>
,我们将修改print_impl
,使得它在不被覆盖的情况下从一个特殊的基类继承,我们只需检查print_impl<T>
是否继承了该基类:
struct special_base_class { };
template <typename T>
struct print_impl : special_base_class {
template <typename ...Args>
static constexpr auto apply(Args&& ...) = delete;
};
template <typename T>
struct Printable
: hana::integral_constant<bool,
!std::is_base_of<special_base_class, print_impl<hana::tag_of_t<T>>>::value
>
{ };
当然,当我们使用自定义类型特化print_impl
时,我们不会继承该special_base_class
类型:
struct Person { std::string name; };
template <>
struct print_impl<Person> /* don't inherit from special_base_class */ {
// ... implementation ...
};
static_assert(Printable<Person>::value, "");
static_assert(!Printable<void>::value, "");
正如你所看到的,Printable<T>
只是检查print_impl <T> struct
是否是一个自定义类型。 特别地,它甚至不检查是否定义嵌套的::apply
函数或者它是否在语法上有效。 假设如果一个专门用于自定义类型的print_impl
,则嵌套的::apply
函数存在并且是正确的。 如果不是,则当尝试在该类型的对象上调用print
时将触发编译错误。 Hana
中的concept
做出相同的假设。
由于这种从特殊基类继承的模式在Hana
中是相当常用的,所以库提供了一个称为hana::default_
的虚拟类型,可以用于替换special_base_class
。 然后,不使用std::is_base_of
,可以使用hana::is_default
,看起来更好。 有了这个语法糖,代码现在变成:
template <typename T>
struct print_impl : hana::default_ {
template <typename ...Args>
static constexpr auto apply(Args&& ...) = delete;
};
template <typename T>
struct Printable
: hana::integral_constant<bool,
!hana::is_default<print_impl<hana::tag_of_t<T>>>::value
>
{ };
这就是要知道标签调度函数和concept
之间的交互。然而,Hana
中的一些concept
不仅仅依赖于特定标签调度函数的定义来确定类型是否是concept
的模型。当concept
仅通过法则和精化concept
引入语义保证,但没有额外的句法要求时,这可能发生。定义这样的concept
由于几个原因是有用的。首先,如果我们可以假设一些语义保证X
或Y
,有时候会发生一个算法可以更有效地实现,所以我们可能创建一个concept
来强制这些保证。其次,当我们有额外的语义保证时,有时可以自动定义几个concept
的模型,这样可以节省用户手动定义这些模型的麻烦。例如,这是Sequence
的concept
的情况,它基本上为Iterable
和Foldable
添加了语义保证,从而允许我们为从Comparable
到Monad
的大量concept
定义模型。
对于这些concept
,通常需要在boost::hana
命名空间中特化相应的模板结构以提供自定义类型的模型。这样做就像提供一个密封,说这个concept
所要求的语义保证是由定制类型遵守的。需要明确特化的concept
将文档化这一事实。这就是所有有必要了解的Hana
的concept
,到这里就结束了关于Hana
的核心一节。