繁体   English   中英

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

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

我是一名回归 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;
}

例如,以下代码(取自运行 xeus-cling C++14 内核的 Jupyter notebook 中的代码单元)

#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)

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

此外,对我所拥有的代码的任何批评都将不胜感激。

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

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

接下来,我们需要计算最终列表的大小。 这……实际上出奇的困难。 有人可能认为您可以只使用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<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 here 接受一个函数和一个元组,并将元组解包到函数的参数中,返回函数返回的任何内容。

所以,回到我们的函数中,我们可以使用apply来做我们需要的事情:间接,然后增加每个元素。 通用 lambdas 允许我们有效地处理通过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);
}

对于 C++ 来说, unused和它的初始值设定项只是在包中的每个项目上执行表达式,而丢弃结果是一种令人困惑的方式。 不幸的是,这是在 C++14 中最直接的方法

这是整个工作示例

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM