【问题标题】:How to: Extend C++14 template function to variadic template, arguments如何:将 C++14 模板函数扩展到可变参数模板、参数
【发布时间】:2020-10-14 04:28:38
【问题描述】:

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

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 notebook 中运行 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)

我想扩展我的函数以获取任意数量的任意类型的容器,并且我花了一些时间研究可变参数模板,但令我尴尬的是,我只是没有连接这些点。我如何推广这个函数来获取任意数量的任意容器类型来保存任意数据类型?我不一定要寻找我需要的确切代码,但我确实可以使用一些帮助来了解如何在这种情况下利用可变参数模板。

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

【问题讨论】:

    标签: c++ c++14 variadic-templates template-meta-programming


    【解决方案1】:

    可变参数模板的机制与 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))...);
    

    ... 的这种用法称为包扩展。其左侧的表达式包含一个或多个包。 ... 采用该表达式并将其转换为逗号分隔的值序列,替换列出包的每个对应成员。被扩展的表达式是begin(std::forward&lt;Ranges&gt;(ranges))。而且我们在这里同时使用了rangesRanges,所以两个包一起展开(并且大小必须相同)。

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

    当然,end 也一样。

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

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

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

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

    实际上,只需重新计算开始/结束迭代器并计算差异,这一切都在一个表达式中更容易。

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

    嗯,就代码行而言“更容易”,而不是可读性;

    std::min 这里需要一个初始化列表来计算最小值。

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

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

    哦,这是完全可行的。你只需要一个新功能。

    您看,您不能使用运行时索引访问tuple 的元素。而且你不能循环编译时值。所以你需要一些方法来获取一个包,而不是参数或类型,而是整数索引。这组索引可以解压到get&lt;Index&gt; 调用中,以便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 这里接受一个函数和一个元组,并将元组解包到函数的参数中,返回函数返回的任何内容。

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

    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.

    【讨论】:

    • emplace_back(*its++...) 并跳过折叠表达式?
    • @PasserBy:我不确定你的意思。我们在谈论什么折叠表达式?
    • 去掉第二个apply,第一个apply改成result.emplace_back(*its++...);
    • @PasserBy:我不喜欢传播编码技术,比如在表达式中间使用++ 运算符。即使这意味着添加一个大而复杂的语句,至少它是清楚发生了什么。
    • @NicolBolas:谢谢,这太棒了。我仍在考虑应用实现,但它主要是点击。与我离开时相比,C++ 变得更加陌生。
    猜你喜欢
    • 2014-04-12
    • 1970-01-01
    • 1970-01-01
    • 2017-05-28
    • 2010-10-15
    • 1970-01-01
    • 2015-05-21
    相关资源
    最近更新 更多