如何:将 C++14 模板函数扩展到可变参数模板、参数

How to: Extend C++14 template function to variadic template, arguments

我是一名回归的 C++ 程序员,已经离开该语言多年(当我最后一次活跃于该语言时,C++11 才刚刚开始获得真正的关注)。在过去的几年里,我一直在 Python 积极开发数据科学应用程序。作为恢复速度的学习练习,我决定在 C++14 中实现 Python 的 zip() 函数,现在有一个工作函数可以容纳任何两个 STL(和其他一些)容器任何类型并将它们“压缩”到元组向量中:

template <typename _Cont1, typename _Cont2>
auto pyzip(_Cont1&& container1, _Cont2&& container2) {
    using std::begin;
    using std::end;
    
    using _T1 = std::decay_t<decltype(*container1.begin())>;
    using _T2 = std::decay_t<decltype(*container2.begin())>;

    auto first1 = begin(std::forward<_Cont1>(container1));
    auto last1 = end(std::forward<_Cont1>(container1));
    auto first2 = begin(std::forward<_Cont2>(container2));
    auto last2 = end(std::forward<_Cont2>(container2));
    
    std::vector<std::tuple<_T1, _T2>> result;
    result.reserve(std::min(std::distance(first1, last1), std::distance(first2, last2)));
    for (; first1 != last1 && first2 != last2; ++first1, ++first2) {
        result.push_back(std::make_tuple(*first1, *first2));
    }
    return result;
}

例如下面的代码(取自Jupyter笔记本运行 xeus-cling C++14内核中的一个代码单元)

#include <list>
#include <xtensor/xarray.hpp>

list<int> v1 {1, 2, 3, 4, 5};
xt::xarray<double> v2 {6.01, 7.02, 8.03};
auto zipped = pyzip(v1, v2);

for (auto tup: zipped)
    cout << '(' << std::get<0>(tup) << ", " << std::get<1>(tup) << ") ";

产生这个输出:

(1, 6.01) (2, 7.02) (3, 8.03)

我想扩展我的功能以获取任意数量的任意类型的容器,并且我花了一些时间研究可变参数模板,但令我尴尬的是我没有把这些点联系起来。我如何概括此函数以采用任意数量的任意容器类型来保存任意数据类型?我不一定要寻找我需要的确切代码,但我真的可以使用一些帮助来了解如何在这种情况下利用可变参数模板。

此外,如果对我的代码提出任何批评,我们将不胜感激。

Variadic 模板的机制与 Python 传递函数位置参数然后将这些位置参数扩展为值序列的能力并无太大不同。 C++ 的机制更强大,更基于模式。

所以让我们从头开始。您想采用任意系列的范围(容器太有限):

template <typename ...Ranges>
auto pyzip(Ranges&& ...ranges)

这里使用...指定了一个包的声明。这个特定的函数声明声明了两个“包”:一个名为 Ranges 的类型包和一个名为 ranges.

的参数包

因此,您需要做的第一件事是获取一系列开始和结束迭代器。由于这些迭代器可以是任意类型,数组不行;它必须存储在 tuple 中。元组的每个元素都需要通过获取 ranges、获取该元素并对其调用 begin 来初始化。方法如下:

auto begin_its = std::make_tuple(begin(std::forward<Ranges>(ranges))...);

这种 ... 的使用称为包 扩展 。其左侧的表达式包含一个或多个包。 ... 采用该表达式并将其转换为 comma-separated 值序列,替换列出包的包中的每个相应成员。展开的表达式是 begin(std::forward<Ranges>(ranges))。并且我们这里同时使用 rangesRanges,所以两个包一起扩展(并且必须是相同的大小)。

我们将此包扩展到 make_tuple 的参数中,以便函数为包中的每个元素获取一个参数。

当然,同样的事情也适用于 end

接下来,您要将每个范围内的元素的副本(?)存储在 vector<tuple> 中。那么,这就需要我们先弄清楚范围的值类型是什么。在示例中使用的 typedef 上使用另一个包扩展很容易:

using vector_elem = std::tuple<std::decay_t<decltype(*begin(std::forward<Ranges>(ranges)))>...>;
std::vector<vector_elem> result;

请注意,在这种情况下,... 不适用于“表达式”,但它做同样的事情:为 [=18= 的每个元素重复 std::decay_t 部分].

接下来,我们需要计算最终列表的大小。这……其实是出奇的困难。有人可能认为您可以只使用 begin_itsend_its,然后迭代它们,或者对它们使用一些包扩展恶作剧。但是不,C++ 不允许您执行其中任何一项。这些元组不是包,您不能(轻易地)这样对待它们。

重新计算 begin/end 迭代器并取差实际上更容易,所有这些都在一个表达式中。

auto size = std::min({std::distance(begin(std::forward<Ranges>(ranges)), end(std::forward<Ranges>(ranges)))...});
result.reserve(std::size_t(size));

好吧,就代码行而言“更简单”,可读性就没那么高了;

std::min 这里取 initializer-list 个值来计算最小值。

对于我们的循环,与其一直循环直到迭代器达到结束状态,不如只循环计数更容易。

但这实际上只是在推迟最后一个问题。也就是说,我们有这个迭代器元组,我们需要对其成员执行 2 个操作:取消引用和递增。我们正在做哪一个并不重要;在 C++ 中同样困难

哦,完全可以。你只需要一个新功能。

看,您无法使用运行时索引访问 tuple 的元素。而且你不能遍历 compile-time 值。所以你需要一些方法来获得一个包,其中包含的不是参数或类型,而是整数索引。这组索引可以解压缩到 get<Index> 调用中,以便 tuple 访问其内容。

C++17 为我们提供了一个方便的 std::apply 函数来完成这类事情。不幸的是,这是C++14,所以我们必须写一个:

namespace detail {
template <class F, class Tuple, std::size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>)
{
    return f(std::get<I>(std::forward<Tuple>(t))...);
}
}  // namespace detail
 
template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t)
{
    return detail::apply_impl(
        std::forward<F>(f), std::forward<Tuple>(t),
        std::make_index_sequence<std::tuple_size<std::remove_reference_t<Tuple>>::value>{});
}

apply 这里接受一个函数和一个元组,并将元组解压到函数的参数中,返回函数 returns.

所以,回到我们的函数,我们可以使用 apply 来做我们需要的事情:间接,然后递增每个元素。通用 lambda 允许我们有效地处理通过 emplace_back:

将值插入 vector
for (decltype(size) ix = 0; ix < size; ++ix)
{
  apply([&result](auto&& ...its) mutable
  {
    result.emplace_back(*its...); //No need for redundant `make_tuple`+copy.
  }, begin_its);

  apply([](auto& ...its)
  {
    int unused[] = {0, (++its, 0)...};
  }, begin_its);
}

unused 及其初始化程序是 C++ 对包中的每个项目执行表达式并丢弃结果的一种混淆方式。不幸的是,it's the most straightforward way to do that in C++14.

Here's the whole working example.