类型计算

在这一点上,如果你有兴趣像MPL一样进行类型计算,你可能会想知道Hana如何帮助你。不用担心,Hana提供了一种通过将类型表示为值来执行具有大量表达性的类型计算的方法,就像我们将编译时数字表示为值一样。 这是一种全新的接触元编程的方法,如果你想熟练使用Hana,你应该尝试将你的旧MPL习惯放在一边。

但是,请注意,现代C++的功能,如自动推导返回类型,在许多情况下不需要类型计算。 因此,在考虑做一个类型计算之前,你应该问自己是否有一个更简单的方法来实现你想要实现的。在大多数情况下,答案是肯定的。 然而,当答案是否定的时候,Hana将为你提供核力量设施来做需要做的事情。

类型作为对象

Hana中类型计算的关键点基本上与编译时计算的方法相同。基本想法是将编译时实体表示为对象,将它们包装到某种容器中。 对于IntegralConstant,编译时实体是整型的常量表达式,我们使用的包装器是integral_constant。在本节中,编译时实体将是类型,我们将使用的包装器称为type,就像我们对IntegralConstant做的一样,让我们开始定义一个可以用来表示类型的虚拟模板:

  1. template<typename T>
  2. struct basic_type{
  3. //empty (for now)
  4. };
  5. basic_type<int> Int{};
  6. basic_type<char> Char{};

注意: 在这里我们使用basic_type名字,是因为我们仅构建一个hana提供版本的最简版本。

虽然这看起来完全没用,但实际上足以开始编写看起来像函数的元函数了。 让我们考虑以下std::add_pointerstd::is_pointer的替代实现:

  1. template<typename T>
  2. constexpr basic_type<T*> add_pointer(basic_type<T> const&)
  3. { return {}; }
  4. template<typename T>
  5. constexpr auto is_pointer(basic_type<T> const&)
  6. { return hana::bool_c<false>; }
  7. template<typename T>
  8. constexpr auto is_pointer(basic_type<T*> const&)
  9. { return hana::bool_c<true>; }

我们刚刚编写了看起来像函数的元函数,就像我们在上一节中将编译时算术元函数编写为异构C++操作符一样。 以下是我们如何使用它们:

  1. basic_type<int> t{};
  2. auto p=add_pointer(t);
  3. BOOST_HANA_CONSTANT_CHECK(is_pointer(p));

注意到我们现在如何使用正常的函数调用语法来执行类型级别的计算了吗? 这类似于使用编译期数值如何使用正常的C++操作符来执行编译时计算。 像我们对integral_constant所做的一样,我们还可以进一步使用C++14变量模板为创建类型提供语法糖:

  1. template<typename T>
  2. constexpr basic_type<T> type_c{};
  3. auto t=type_c<int>;
  4. auto p=add_pointer(t);
  5. BOOST_HANA_CONSTANT_CHECK(is_pointer(p));

注意: 这不是hana::type_c变量模板完整实现,因为有些细微不同之处;仅用于解释的目的。把它仍到一边,参考hana::type的实现,以确切知道您可以从hana::type_c<...>中获得什么。

优势

这样做有什么好处呢?因为type_c<...>是一个对象,我们可以将它存储到像tuple这样的异构容器中,我们可以移动它,也可以将它传递(或者返回)到函数中,而且我们可以用在任何需要对象的地方:

  1. auto types=hana::make_tuple(hana::type_c<int*>,hana::type_c<char&>,hana::type_c<void>);
  2. auto char_ref=types[1_c];
  3. BOOST_HANA_CONSTANT_CHECK(char_ref==hana::type_c<char&>);

注意: 当需要多个类型时,编写make_tuple(type_c<T>...)可能会觉得比较繁琐,为此,Hana提供了变量模板tuple_t<T...>,它是make_tuple(type_c<T>...)的语法糖。

另外,请注意,由于上面的元组实际上只是一个正常的异构序列,就像ints的元组上一样,我们可以对该序列应用异构算法。此外,由于我们只是操作对象,我们现在可以使用完整的语言支持,而不仅仅是在类型级别提供的小子集。例如,考虑删除不是引用的所有类型或来自类型序列的指针的任务。如果用MPL,我们必须使用占位符表达式来表达谓词,看起来较为笨拙:

  1. using types = mpl::vector<int, char&, void*>;
  2. using ts = mpl::copy_if<types, mpl::or_<std::is_pointer<mpl::_1>,
  3. std::is_reference<mpl::_1>>>::type;
  4. // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  5. // placeholder expression
  6. static_assert(mpl::equal<ts, mpl::vector<char&, void*>>::value, "");

现在,由于我们在操作对象,我们可以使用完整的语言支持,并使用一个通用的lambda,这样的代码有更好的可读性:

  1. auto types = hana::tuple_t<int*, char&, void>;
  2. auto ts = hana::filter(types, [](auto t) {
  3. return is_pointer(t) || is_reference(t);
  4. });
  5. BOOST_HANA_CONSTANT_CHECK(ts == hana::tuple_t<int*, char&>);

由于Hana对所有异构容器进行统一处理,这种将类型表示为值的方法也具有以下优点:异构计算和类型计算现在只需要单个库即可。事实上,虽然我们通常需要两个不同的库来执行几乎相同的任务,但我们现在想要一个单独的库来完成它们。再次,考虑使用谓词过滤序列的任务.如果使用MPLFusion,我们必须这样做:

  1. // types (MPL)
  2. using types = mpl::vector<int*, char&, void>;
  3. using ts = mpl::copy_if<types, mpl::or_<std::is_pointer<mpl::_1>,
  4. std::is_reference<mpl::_1>>>::type;
  5. // values (Fusion)
  6. auto values = fusion::make_vector(1, 'c', nullptr, 3.5);
  7. auto vs = fusion::filter_if<std::is_integral<mpl::_1>>(values);

但用Hana,只要一个库就行了。注意看我们怎么使用相似的算法和容器的,并且只调整谓词,以便它可以对值进行操作:

  1. // types
  2. auto types = hana::tuple_t<int*, char&, void>;
  3. auto ts = hana::filter(types, [](auto t) {
  4. return is_pointer(t) || is_reference(t);
  5. });
  6. // values
  7. auto values = hana::make_tuple(1, 'c', nullptr, 3.5);
  8. auto vs = hana::filter(values, [](auto const& t) {
  9. return is_integral(hana::typeid_(t));
  10. });

但这不是全部。 实际上,具有用于类型和值计算的统一语法允许我们在异构容器的接口中实现更大的一致性。 例如,考虑创建将类型与值相关联,然后访问它的元素的异构映射的简单任务。 使用Fusion,您那未经训练的眼睛不会看一眼就能理解的:

  1. auto map = fusion::make_map<char, int, long, float, double, void>(
  2. "char", "int", "long", "float", "double", "void"
  3. );
  4. std::string Int = fusion::at_key<int>(map);
  5. BOOST_HANA_RUNTIME_CHECK(Int == "int");

但是,对于类型和值统一语法,同样的事情变得更加清楚:

  1. auto map = hana::make_map(
  2. hana::make_pair(hana::type_c<char>, "char"),
  3. hana::make_pair(hana::type_c<int>, "int"),
  4. hana::make_pair(hana::type_c<long>, "long"),
  5. hana::make_pair(hana::type_c<float>, "float"),
  6. hana::make_pair(hana::type_c<double>, "double")
  7. );
  8. std::string Int = map[hana::type_c<int>];
  9. BOOST_HANA_RUNTIME_CHECK(Int == "int");

虽然以Hana的方式需要更多的代码行,但它更可读,更接近我们期望的初始化方式。

使用此方式工作

到目前为止,我们可以将类型表示为值,并使用通常的C++语法对这些对象执行类型级计算。 这很好,但它不是非常有用,因为我们没有办法从对象表示推导出正常的C++类型。 例如,我们如何声明一个从类型计算的结果获取的变量类型?

  1. auto t = add_pointer(hana::type_c<int>); // could be a complex type computation
  2. using T = the-type-represented-by-t;
  3. T var = ...;

现在,没有简单的方法去做以上工作。 为了更容易实现,我们丰富了上面定义的basic_type容器的接口。 而不是一个空的结构,我们现在定义为:

  1. template <typename T>
  2. struct basic_type {
  3. using type = T;
  4. };

注意: 这相当于使basic_typeMPL意义上的元函数。

这样,我们可以使用decltype来容易地访问由type_c<...>对象表示的实际C++类型:

  1. auto t = add_pointer(hana::type_c<int>);
  2. using T = decltype(t)::type; // fetches basic_type<T>::type
  3. T var = ...;

一般来说,使用Hana进行类型元编程需要三步:

  1. 使用hana::type_c<...>将类型包装为对象;
  2. 使用值语法执行类型转换;
  3. 使用decltype(...)::type将结果解析成类型。

现在,你一定认为这是令人难以置信的繁琐。 在现实中,它有以下几个原因。 首先,这种包装和解包只需要在一些非常薄的边界发生。

  1. auto t = hana::type_c<T>;
  2. auto result = huge_type_computation(t);
  3. using Result = decltype(result)::type;

此外,由于您在计算中获得处理对象(无需包装/解包)的优势,因此包装和解包的成本将摊销在整个计算上。 因此,对于复杂类型的计算,根据在该计算内的值的工作的表达性增益,该三步骤过程的句法噪声很快变得可忽略。另外,使用值而不是类型意味着我们可以避免在整个地方键入typenametemplate,这在经典元编程中占了很多句法噪声。

另一点是,并不总是需要三个完整的步骤。 事实上,有时候只需要一个类型的计算并查询结果,而不必将结果作为一个普通的C++类型:

  1. auto t = hana::type_c<T>;
  2. auto result = type_computation(t);
  3. BOOST_HANA_CONSTANT_CHECK(is_pointer(result)); // third step skipped

在这种情况下,我们可以跳过第三步,因为我们不需要访问由result表示的实际类型。 在其他情况下,可以避免第一步,就像使用tuple_t,它没有比任何其他纯类型级别方法更多的语法噪声:

  1. auto types = hana::tuple_t<int*, char&, void>; // first step skipped
  2. auto pointers = hana::transform(types, [](auto t) {
  3. return add_pointer(t);
  4. });

对此持怀疑态度的读者,让我们考虑一个找到类型序列中最小类型的任务。 这个短小的类型计算例子能很好地说明这个问题,也是我们期望新的范式受到最大的影响的地方。 正如你将看到的,即使对于小计算,事情仍然可管理。 首先,让我们用MPL实现它:

  1. template <typename ...T>
  2. struct smallest
  3. : mpl::deref<
  4. typename mpl::min_element<
  5. mpl::vector<T...>,
  6. mpl::less<mpl::sizeof_<mpl::_1>, mpl::sizeof_<mpl::_2>>
  7. >::type
  8. >
  9. { };
  10. template <typename ...T>
  11. using smallest_t = typename smallest<T...>::type;
  12. static_assert(std::is_same<
  13. smallest_t<char, long, long double>,
  14. char
  15. >::value, "");

结果是很有可读性的(对于任何熟悉MPL的人)。 现在让我们使用Hana实现相同的事情:

  1. template <typename ...T>
  2. auto smallest = hana::minimum(hana::make_tuple(hana::type_c<T>...), [](auto t, auto u) {
  3. return hana::sizeof_(t) < hana::sizeof_(u);
  4. });
  5. template <typename ...T>
  6. using smallest_t = typename decltype(smallest<T...>)::type;
  7. static_assert(std::is_same<
  8. smallest_t<char, long, long double>, char
  9. >::value, "");

正如你所看到的,3步过程的句法噪声几乎完全被其余的计算所掩盖了。

一般提升步骤

我们以函数形式引入的第一个类型计算如下:

  1. template <typename T>
  2. constexpr auto add_pointer(hana::basic_type<T> const&) {
  3. return hana::type<T*>;
  4. }

我们需要将它写成如下样子,虽然看起来复杂了一些:

  1. template <typename T>
  2. constexpr auto add_pointer(hana::basic_type<T> const&) {
  3. return hana::type_c<typename std::add_pointer<T>::type>;
  4. }

然而,这个实现强调的事实是,我们真的在模拟一个现有的元函数,并简单地表示为一个函数。 换句话说,我们通过创建我们自己的add_pointer函数来提取一个元函数(std::add_pointer)到值的世界。 事实证明,这个提升过程是一个通用的过程。 事实上,给定任何元函数,我们可以写几乎相同的事情:

  1. template <typename T>
  2. constexpr auto add_const(hana::basic_type<T> const&)
  3. { return hana::type_c<typename std::add_const<T>::type>; }
  4. template <typename T>
  5. constexpr auto add_volatile(hana::basic_type<T> const&)
  6. { return hana::type_c<typename std::add_volatile<T>::type>; }
  7. template <typename T>
  8. constexpr auto add_lvalue_reference(hana::basic_type<T> const&)
  9. { return hana::type_c<typename std::add_lvalue_reference<T>::type>; }
  10. // etc...

这种机械变换很容易被抽象成可以处理任何MPL元函数的通用提升器,如下所示:

  1. template <template <typename> class F, typename T>
  2. constexpr auto metafunction(hana::basic_type<T> const&)
  3. { return hana::type_c<typename F<T>::type>; }
  4. auto t = hana::type_c<int>;
  5. BOOST_HANA_CONSTANT_CHECK(metafunction<std::add_pointer>(t) == hana::type_c<int*>);

更一般地,我们将允许具有任何数量的参数的元函数,这带来了以下稍微呆板的实现:

  1. template <template <typename ...> class F, typename ...T>
  2. constexpr auto metafunction(hana::basic_type<T> const& ...)
  3. { return hana::type_c<typename F<T...>::type>; }
  4. BOOST_HANA_CONSTANT_CHECK(
  5. metafunction<std::common_type>(hana::type_c<int>, hana::type_c<long>) == hana::type_c<long>
  6. );

Hana提供了一个类似的通用元函数升级器hana::metafunction。 一个小小的改进是hana::metafunction<F>是一个函数对象,而不是一个重载的函数,所以可以把它传递给更高阶的算法。 它也是一个稍微更强大的Metafunction概念的模型,但是现在可以安全地忽略它。 我们在本节中探讨的过程不仅适用于元函数; 它也适用于模板。 事实上,我们可以定义:

  1. template <template <typename ...> class F, typename ...T>
  2. constexpr auto template_(hana::basic_type<T> const& ...)
  3. { return hana::type_c<F<T...>>; }
  4. BOOST_HANA_CONSTANT_CHECK(
  5. template_<std::vector>(hana::type_c<int>) == hana::type_c<std::vector<int>>
  6. );

Hana为名为hana::template_的模板提供了一个通用的提升器,它还为名为hana::metafunction_classMPL元函数类提供了一个通用的提升器。 这为我们提供了一种将“传统”类型计算统一表示为函数的方法,以便使用经典类型元编程库编写的任何代码几乎可以与Hana一起使用。 例如,假设你有一大块基于MPL的代码,你想与Hana接口。 这样做的过程不会比用Hana提供的提升器包装你的元函数更难:

  1. template <typename T>
  2. struct legacy {
  3. using type = ...; // something you really don't want to mess with
  4. };
  5. auto types = hana::make_tuple(...);
  6. auto use = hana::transform(types, hana::metafunction<legacy>);

但是,请注意,并非所有类型级别的计算都可以使用Hana提供的工具提升。 例如,不能提升std::extent,因为它需要非类型模板参数。 因为没有办法在C++中统一处理非类型模板参数,所以必须使用特定于该类型级别计算的手写函数对象:

  1. auto extent = [](auto t, auto n) {
  2. return std::extent<typename decltype(t)::type, hana::value(n)>{};
  3. };
  4. BOOST_HANA_CONSTANT_CHECK(extent(hana::type_c<char>, hana::int_c<1>) == hana::size_c<0>);
  5. BOOST_HANA_CONSTANT_CHECK(extent(hana::type_c<char[1][2]>, hana::int_c<1>) == hana::size_c<2>);

注意: 当从头文件<type_traits>中使用type traits时,不要忘记包含文件:std::integral_constrant(<boost/hana/ext/std/integral_constant.hpp>)

然而,在实践中,这不应该是一个问题,因为绝大多数类型计算可以很容易地提升。 最后,由于<type_traits>头文件提供的metafunctions使用频繁,Hana均为它们提供了对应的提升的版本。 那些解析后的traitshana::traits命名空间,他们在<boost/hana/traits.hpp>头文件中:

  1. BOOST_HANA_CONSTANT_CHECK(hana::traits::add_pointer(hana::type_c<int>) == hana::type_c<int*>);
  2. BOOST_HANA_CONSTANT_CHECK(hana::traits::common_type(hana::type_c<int>, hana::type_c<long>) == hana::type_c<long>);
  3. BOOST_HANA_CONSTANT_CHECK(hana::traits::is_integral(hana::type_c<int>));
  4. auto types = hana::tuple_t<int, char, long>;
  5. BOOST_HANA_CONSTANT_CHECK(hana::all_of(types, hana::traits::is_integral));

到这里类型计算部分就结束了。 虽然这种用于类型编程的新范例可能很难在第一次就投入使用,但随着使用它越来越有意义, 你也会越来越喜欢它模糊类型和值之间的界限,带来的新的令人兴奋的可能性而且用它来简化许多任务。