Hana内核

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

本节的目标是对Hana的核心进行一个高层次的概述。 这个核心是基于tag的概念,它是从Boost.FusionBoost.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来表示异构容器和其他编译时实体。例如,所有的Hanaintegral_constant<int,...>都有不同的类型,但是它们都有相同的tag,即integral_constant_tag<int>。这就允许程序员根据单个类型来思考问题,而不是试图考虑每个对象的实际类型。具体来说,tag被实现为空结构体。为了区别它们,Hana约定通过添加_tag后缀来命名这些tag

注意: 可以通过使用tag_of<T>::type或等效的tag_of_t<T>来获得与T的类型关联的tag对象类型。

tag是正常C++类型的扩展。 事实上,默认情况下,类型TtagT本身,库的核心就是设计为在这些情况下工作。 例如,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.Fusionvector不一定是boost::fusion::vector模板的特化。 Fusionvector以编号形式存在,它们都是不同类型:

boost::fusion::vector1<T>
boost::fusion::vector2<T, U>
boost::fusion::vector3<T, U, V>
...

这是一个实现细节,因为C++03(很不幸的)缺少可变参数模板,但我们需要一种方法来解决它。为此,我们使用具有三个不同组件的基础结构:

  1. 一个元函数将单个tag关联到相关类型系列中的每个类型。在Hana中,可以使用tag_of元函数访问此标记。具体来说,对于任何类型Ttag_of<T>::type是用于分派它的标签。
  2. 属于库的公共接口的函数,我们希望能够提供自定义实现。在Hana中,这些函数是与concept相关的算法,如transformunpack
  3. 函数的实现,将参数化的标签传递给函数参数。在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

Hanaconcept的实现非常简单。 在它的核心,一个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::unpackhana::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由于几个原因是有用的。首先,如果我们可以假设一些语义保证XY,有时候会发生一个算法可以更有效地实现,所以我们可能创建一个concept来强制这些保证。其次,当我们有额外的语义保证时,有时可以自动定义几个concept的模型,这样可以节省用户手动定义这些模型的麻烦。例如,这是Sequenceconcept的情况,它基本上为IterableFoldable添加了语义保证,从而允许我们为从ComparableMonad的大量concept定义模型。

对于这些concept,通常需要在boost::hana命名空间中特化相应的模板结构以提供自定义类型的模型。这样做就像提供一个密封,说这个concept所要求的语义保证是由定制类型遵守的。需要明确特化的concept将文档化这一事实。这就是所有有必要了解的Hanaconcept,到这里就结束了关于Hana的核心一节。