快速问答

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

本节记录了一些设计选择的理由。 它也作为一些(不是这样)常见问题的解答。 如果你认为某个问题应该添加到这个列表,开一个GitHub issue,我们将考虑改进文档或在这里添加问题。

为什么要限制外部依赖的使用?

这样做有几个原因。首先,Hana是一个非常基础的库;我们基本上重新实现了核心语言和支持异构类型的标准库。当编写代码时,人们会很快意识到,其他库很少需要,几乎一切都必须从头开始实现。此外,由于Hana是非常基本的,因此更有动机保持依赖最小,因为那些依赖将传递给用户。关于对Boost最小的依赖,使用它的一个大论据是可移植性。然而,作为一个前沿库,Hana只定位于最新的编译器。因此,我们可以依赖于现代构造,并且摆脱使用Boost带来的可移植性负担。

为什么没有迭代器?

基于迭代器的设计具有它们自己的优点,但是也已知它们降低算法的可组合性。此外,异构编程的上下文带来了很多点,使迭代器不那么有趣。例如,递增迭代器将必须返回具有不同类型的新迭代器,因为它在序列中指向的新对象的类型可能不同。它也证明,在迭代器方面实现大多数算法导致更差的编译时性能,只是因为元编程的执行模型(使用编译器作为解释器)与C++的运行时执行模型(处理器访问连续存储器)。

为什么要留下一些容器的表示实现定义?

首先,它为实现提供了更多的摆动空间,通过使用特定容器的聪明表示来执行编译时和运行时优化。 例如,包含类型T的同类对象的元组可以实现为类型T的数组,这在编译时更有效。 其次,最重要的是,知道一个异构容器的类型不如你想象的有用。 实际上,在异构编程的上下文中,由计算返回的对象的类型通常也是计算的一部分。 换句话说,没有办法知道由算法返回的对象的类型,而不实际执行算法。 例如,考虑find_if算法:

auto tuple = hana::make_tuple(1, 'x', 3.4f);
auto result = hana::find_if(tuple, [](auto const& x) {
  return hana::traits::is_integral(hana::typeid_(x));
});

如果对元组的某些元素满足谓词,结果将等于just(x)。 否则,结果将为空。 然而,结果为空(nothing)是在编译时已知的,这需要just(x)nothing有不同的类型。 现在,假设你想明确地写出结果的类型:

some_type result = hana::find_if(tuple, [](auto const& x) {
  return hana::traits::is_integral(hana::typeid_(x));
});

为了拥有some_type的知识,您需要实际执行算法,因为some_type取决于容器中某些元素的谓词是否得到满足。 换句话说,如果你能够写上面的话,那么你就已经知道了算法的结果,你不需要首先执行算法。 在Boost.Fusion中,通过具有单独的result_of命名空间来解决这个问题,该命名空间包含计算给定传递给它的参数的类型的任何算法的结果类型的元函数。 例如,上面的例子可以用Fusion重写为:

using Container = fusion::result_of::make_vector<int, char, float>::type;
Container tuple = fusion::make_vector(1, 'x', 3.4f);
using Predicate = mpl::quote1<std::is_integral>;
using Result = fusion::result_of::find_if<Container, Predicate>::type;
Result result = fusion::find_if<Predicate>(tuple);

注意,我们基本上是做计算两次; 一次在result_of命名空间中,一次在正常的fusion命名空间中,这是高度冗余的。 在autodecltype之前,这样的技术对于执行异构计算是必要的。 然而,由于现代C++的出现,在异构编程的上下文中对显式返回类型的需要在很大程度上已经过时,并且知道容器的实际类型通常不是有用的。

为什么命名为 Hana?

不,这不是我女朋友的名字!我只需要一个短而好看的名字,人们会很容易记得,而且Hana来了。这也使我注意到,Hana是指日本花,在韩语Hana表达了优美的概念,它统一了单一范式下的类型级和异构编程,这个名称回顾起来是相当不错的选择:-)。

为什么要定义我们自己的元组?

由于Hana在元组上定义了很多算法,一个可能的方法是简单地使用std::tuple并提供算法,而不是提供我们自己的元组。提供我们自己的元组的原因主要是性能。事实上,迄今为止测试的所有std::tuple实现都有非常糟糕的编译时性能。此外,为了获得真正惊人的编译时性能,我们需要利用元组的内部表示在一些算法,这需要定义我们自己的。最后,如果我们使用一个std::tuple,就不能提供一些像运算符[]的糖,因为该运算符必须定义为成员函数。

如何选择名称?

当决定一个名字X时,我尝试平衡以下事情(没有特定的顺序):

  • C++中的X是如何惯用的?
  • 在编程世界的其余部分,X是如何惯用的?
  • 一个名字X实际上是多好,不管历史原因
  • 我如何作为库作者,感觉到X
  • 库的用户如何感受X
  • 是否有技术原因不使用X,如名称冲突或标准保留的名称

当然,好的命名永远是困难的。名称是并将永远受到作者自己的偏见的污染。不过,我尝试以合理的方式选择名字。

如何决定参数顺序?

不像命名,这是相当主观的,函数的参数的顺序通常是相当简单的确定。基本上,经验法则是“容器先行”。在FusionMPL中一直是这种方式,对于大多数C++程序员来说这是直观的。此外,在高阶算法中,我尝试将函数参数放在最后,以便多行lambdas看起来不错:

algorithm(container, [](auto x) {
  return ...;
});
// is nicer than
algorithm([](auto x) {
  return ...;
}, container);

为什么使用标签调度?

我们可以使用几种不同的技术在库中提供自定义点,并且选择了标签分发。为什么?首先,我想要一个双层调度系统,因为这允许第一层(用户调用的那些)的函数实际上是函数对象,这允许将它们传递给更高阶的算法。使用具有两个层的调度系统还允许向第一层添加一些编译时健全性检查,这改进了错误消息。

现在,由于几个原因,选择标签调度而不是其他具有两层的技术。首先,必须明确地声明一些标签是一个概念的模型是否有责任确保概念的语义要求被尊重给用户。其次,当检查类型是否是某个概念的模型时,我们基本上检查一些关键函数是否被实现。特别地,我们检查从该概念的最小完整定义的功能被实现。例如,Iterable <T>检查是否为T实现了is_emptyatdrop_front函数。但是,没有标签分派的情况下检测这个函数的唯一方法是基本检查以下表达式是否在SFINAE-able上下文中有效:

implementation_of_at(std::declval<T>(), std::declval<N>())
implementation_of_is_empty(std::declval<T>())
implementation_of_drop_front(std::declval<T>())

不幸的是,这需要实际做算法,这可能触发硬编译时错误或损害编译时性能。 此外,这需要选择一个任意索引N调用at:如果Iterable是空的怎么办? 使用标签分派,我们可以询问是否定义了at_impl<T>is_empty_impl<T>drop_front_impl<T>,并且没有发生任何事情,直到我们实际调用它们的嵌套::apply函数。

为什么不提供 zip_longest?

它需要(1)用任意对象填充最短序列,或(2)在调用zip_longest时用用户提供的对象填充最短序列。由于不需要所有压缩序列具有类似类型的元素,因此在所有情况下都没有办法提供单个一致的填充对象。应该提供一个填充对象的元组,但我发现它可能太复杂,值得现在。如果您需要此功能,请开一个GitHub issue

为什么 concept 不是 constexpr 函数?

由于C++ concept提议将概念映射到布尔constexpr函数,因此Hana定义其concept也是有意义的,而不是具有嵌套::value的结构体。事实上,这是第一个选择,但它必须修改,因为模板函数有一个限制,使它们更不灵活

template <??? Concept>
struct some_metafunction {
  // ...
};

这种代码在某些上下文中非常有用,例如检查两种类型是否具有常见的嵌入建模概念:

template <??? Concept, typename T, typename U>
struct have_common_embedding {
  // whether T and U both model Concept, and share a common type that also models Concept
};

使用concept作为布尔constexpr函数,这不能一般写。 当概念只是模板结构时,我们可以使用模板模板参数:

template <template <typename ...> class Concept, typename T, typename U>
struct have_common_embedding {
  // whether T and U both model Concept, and share a common type that also models Concept
};