是否可以在C ++中编写通用的可变参数zipWith?

时间:2012-03-15 22:28:15

标签: c++ variadic-templates

我想在变量arity的C ++中使用通用的zipWith函数。我有两个问题。首先是我无法确定传递给zipWith的函数指针的类型。它必须与传递给zipWith的向量数相同,并且必须分别接受对向量元素类型的引用。第二个是我不知道如何并行处理这些向量来构建一个参数列表,调用func(),并在最短的向量用完后保释。

template <typename R, typename T, typename... Vargs>
std::vector<R> zipWith (R func(???<what goes here>), std::vector<T> first, Vargs rest) {
   ???
}

2 个答案:

答案 0 :(得分:7)

我得到了一个很长的答案,然后我以一种使解决方案缩短的方式改变了主意。但我会展示我的思考过程并给你两个答案!

我的第一步是确定正确的签名。我并不了解所有内容,但您可以将参数包视为以逗号分隔的实际项目列表,并隐藏文本转储。您可以通过更多以逗号分隔的项目来扩展列表!所以直接应用:

template <typename R, typename T, typename... Vargs>
std::vector<R> zipWith (R func(T,Vargs...), std::vector<T> first, Vargs rest) {
   ???
}

你必须放一个&#34; ...&#34;在表达式部分的参数包之后查看扩展列表。您还必须在常规参数部分中添加一个:

template <typename R, typename T, typename... Vargs>
std::vector<R> zipWith (R func(T,Vargs...), std::vector<T> first, Vargs... rest) {
   ???
}

你说你的函数参数是一堆向量。在此,您希望每个Vargs都是std::vector。类型转换可以应用于参数包,那么为什么我们不确保您有向量:

template <typename R, typename T, typename... Vargs>
std::vector<R> zipWith (R func(T,Vargs...), std::vector<T> first, std::vector<Vargs> ...rest) {
   ???
}

向量可以是大对象,所以让我们使用const l值引用。另外,我们可以使用std::function,因此我们可以使用lambda或std::bind表达式:

template <typename R, typename T, typename... Vargs>
std::vector<R> zipWith (std::function<R(T, Vargs...)> func, std::vector<T> const &first, std::vector<Vargs> const &...rest) {
   ???
}

(我在这里遇到了使用std::pow进行测试的问题。我的编译器不会接受将经典函数指针转换为std::function对象。所以我不得不把它包装成一个lambda。也许我应该在这里问一下......)

此时,我重新加载页面并看到一个response (by pmr)。我不太了解这种拉链,折叠,爆炸,无论什么东西,所以我认为他/她的解决方案太复杂了。所以我想到了一个更直接的解决方案:

template < typename R, typename T, typename ...MoreTs >
std::vector<R>
zip_with( std::function<R(T,MoreTs...)> func,
 const std::vector<T>& first, const std::vector<MoreTs>& ...rest )
{
    auto const      tuples = rearrange_vectors( first, rest... );
    std::vector<R>  result;

    result.reserve( tuples.size() );
    for ( auto const &x : tuples )
        result.push_back( evaluate(x, func) );
    return result;
}

我会创建一个元组向量,其中每个元组都是从每个向量中提取相应的元素。然后我会创建一个矢量 评估结果来自每次传递一个元组和func

rearrange_vectors必须提前制作值表(默认构造)并一次填写每个条目的子对象:

template < typename T, typename ...MoreTs >
std::vector<std::tuple<T, MoreTs...>>
rearrange_vectors( const std::vector<T>& first,
 const std::vector<MoreTs>& ...rest )
{
    decltype(rearrange_vectors(first, rest...))
      result( first.size() );

    fill_vector_perpendicularly<0>( result, first, rest... );
    return result;
}

第一行的第一部分允许函数访问自己的返回类型而无需复制和粘贴。唯一需要注意的是,r值参考参数必须被std::forward(或move)包围,因此递归调用的l值重载不会被错误地选中。改变每个元组元素的一部分的函数必须显式地获取当前索引。在参数包剥离期间,索引向上移动一次:

template < std::size_t, typename ...U >
void  fill_vector_perpendicularly( std::vector<std::tuple<U...>>& )
{ }

template < std::size_t I, class Seq, class ...MoreSeqs, typename ...U >
void  fill_vector_perpendicularly( std::vector<std::tuple<U...>>&
 table, const Seq& first, const MoreSeqs& ...rest )
{
    auto        t = table.begin();
    auto const  te = table.end();

    for ( auto  f = first.begin(), fe = first.end(); (te != t) && (fe
     != f) ; ++t, ++f )
        std::get<I>( *t ) = *f;
    table.erase( t, te );
    fill_vector_perpendicularly<I + 1u>( table, rest... );
}

该表与最短的输入向量一样长,因此我们必须在当前输入向量首先结束时修整表。 (我希望我可以在fe块中将const标记为for。)我原来firstreststd::vector,但是我意识到我可以把它抽象出来;我需要的只是与迭代接口中的标准(序列)容器匹配的类型。但现在我对evaluate

感到难过
template < typename R, typename T, typename ...MoreTs >
R  evaluate( const std::tuple<T, MoreTs...>& x,
 std::function<R(T,MoreTs...)> func )
{
     //???
}

我可以做个别案件:

template < typename R >
R  evaluate( const std::tuple<>& x, std::function<R()> func )
{ return func(); }

template < typename R, typename T >
R  evaluate( const std::tuple<T>& x, std::function<R(T)> func )
{ return func( std::get<0>(x) ); }

但我无法将其概括为递归案例。 IIUC,std::tuple不支持作为子元组剥离尾部(和/或头部)。 std::bind也不支持将参数卷入零碎的函数中,并且其占位符系统与任意长度的参数包不兼容。我希望我可以像我可以访问原始输入向量一样列出每个参数....

......等等,为什么不我这样做?!...

......好吧,我从来没有听说过。我已经看到将模板参数包传递给函数参数;我刚刚在zipWith中展示了它。我可以从功能参数列表到函数的内部吗? (正如我写的那样,我现在记得在类构造函数的成员初始化部分中看到它,对于数组或类类型的非静态成员。)只有一种方法可以找到:

template < typename R, typename T, typename ...MoreTs >
std::vector<R>
zip_with( std::function<R(T,MoreTs...)> func, const std::vector<T>&
 first, const std::vector<MoreTs>& ...rest )
{
    auto const  s = minimum_common_size( first, rest... );
    decltype(zip_with(func,first,rest...))         result;

    result.reserve( s );
    for ( std::size_t  i = 0 ; i < s ; ++i )
        result.push_back( func(first[i], rest[i]...) );
    return result;
}

我强迫事先计算总呼叫次数:

inline  std::size_t minimum_common_size()  { return 0u; }

template < class SizedSequence >
std::size_t  minimum_common_size( const SizedSequence& first )
{ return first.size(); }

template < class Seq, class ...MoreSeqs >
std::size_t
minimum_common_size( const Seq& first, const MoreSeqs& ...rest )
{ return std::min( first.size(), minimum_common_size(rest...) ); }

果然,它奏效了!当然,这意味着我过度思考问题与其他受访者一样(以不同的方式)。这也意味着我对这篇文章的大部分内容都不必要地厌倦了你。当我把它包起来时,我意识到用std::vector可以应用用通用序列容器类型替换zip_width。我意识到我可以将强制一个向量减少到没有强制向量:

template < typename R, typename ...T, class ...SizedSequences >
std::vector<R>
zip_with( R func(T...) /*std::function<R(T...)> func*/,
 SizedSequences const& ...containers )
{
    static_assert( sizeof...(T) == sizeof...(SizedSequences),
     "The input and processing lengths don't match." );

    auto const  s = minimum_common_size( containers... );
    decltype( zip_with(func, containers...) )     result;

    result.reserve( s );
    for ( std::size_t  i = 0 ; i < s ; ++i )
        result.push_back( func(containers[i]...) );
    return result;
}

我在这里复制代码时添加了static_assert,因为我忘了确保func的参数计数和输入向量的数量一致。现在我意识到我可以通过抽象两个来解决决斗函数指针与std::function对象:

template < typename R, typename Func, class ...SizedSequences >
std::vector<R>
zip_with( Func&& func, SizedSequences&& ...containers )
{
    auto const     s = minimum_common_size( containers... );
    decltype( zip_with<R>(std::forward<Func>(func),
     std::forward<SizedSequences>(containers)...) )  result;

    result.reserve( s );
    for ( std::size_t  i = 0 ; i < s ; ++i )
        result.push_back( func(containers[i]...) );
    return result;
}

使用r值引用标记函数参数是通用传递方法。它处理各种引用和const / volatile(cv)资格。这就是我将containers切换到它的原因。 func可以有任何结构;它甚至可以是具有多个operator ()版本的类对象。由于我使用了容器的r值,因此他们会使用最佳的cv资格来进行元素解除引用,并且该函数可以使用它来进行重载解析。递归&#34;电话&#34;在内部确定结果类型需要使用std::forward来防止任何&#34;降级&#34; l值引用。它还揭示了此迭代中的一个缺陷:我必须提供返回类型。

我会解决这个问题,但首先我要解释一下STL方式。您不预先确定特定容器类型并将其返回给用户。您需要一个特殊对象,一个输出迭代器,您将结果发送到。迭代器可以连接到一个容器,其标准提供了几种。它可以连接到输出流,而不是直接打印结果!迭代器方法也让我免于直接担心内存问题。

#include <algorithm>
#include <cstddef>
#include <iterator>
#include <utility>
#include <vector>

inline  std::size_t minimum_common_size()  { return 0u; }

template < class SizedSequence >
std::size_t  minimum_common_size( const SizedSequence& first )
{ return first.size(); }

template < class Seq, class ...MoreSeqs >
std::size_t  minimum_common_size( const Seq& first,
 const MoreSeqs& ...rest )
{
    return std::min<std::size_t>( first.size(),
     minimum_common_size(rest...) );
}

template < typename OutIter, typename Func, class ...SizedSequences >
OutIter
zip_with( OutIter o, Func&& func, SizedSequences&& ...containers )
{
    auto const  s = minimum_common_size( containers... );

    for ( std::size_t  i = 0 ; i < s ; ++i )
        *o++ = func( containers[i]... );
    return o;
}

template < typename Func, class ...SizedSequences >
auto  zipWith( Func&& func, SizedSequences&& ...containers )
 -> std::vector<decltype( func(containers.front()...) )>
{
    using std::forward;

    decltype( zipWith(forward<Func>( func ), forward<SizedSequences>(
     containers )...) )  result;
#if 1
    // `std::vector` is the only standard container with the `reserve`
    // member function.  Using it saves time when doing multiple small
    // inserts, since you'll do reallocation at most (hopefully) once.
    // The cost is that `s` is already computed within `zip_with`, but
    // we can't get at it.  (Remember that most container types
    // wouldn't need it.)  Change the preprocessor flag to change the
    // trade-off.
    result.reserve( minimum_common_size(containers...) );
#endif
    zip_with( std::back_inserter(result), forward<Func>(func),
     forward<SizedSequences>(containers)... );
    return result;
}

我在这里复制了minimum_common_size,但明确提到了最小基本案例的结果类型,使用不同的大小类型对不同的容器类型进行校对。

采用输出迭代器的函数通常在所有迭代器完成后返回迭代器。这使您可以在中断的位置开始新的输出运行(即使使用不同的输出功能)。它对标准输出迭代器并不重要,因为它们都是伪迭代器。使用前向迭代器(或以上)作为输出迭代器非常重要,因为它们执行跟踪位置。 (使用前向迭代器作为输出,只要最大传输次数不超过剩余的迭代空间,就是安全的。)有些函数将输出迭代器放在参数列表的末尾,其他函数放在开头; zip_width必须使用后者,因为参数包必须在最后。

在计算返回类型表达式时,移动到zipWith中的后缀返回类型会使函数的签名公平游戏的每个部分都生成。如果由于编译时的不兼容性而无法完成计算,它也会立即让我知道。 std::back_inserter函数向向量返回一个特殊的输出迭代器,它通过push_back成员函数添加元素。

答案 1 :(得分:3)

这是我拼凑的东西:

#include <iostream>
#include <vector>
#include <utility>

template<typename F, typename T, typename Arg>
auto fold(F f, T&& t, Arg&& a) 
  -> decltype(f(std::forward<T>(t), std::forward<Arg>(a)))
{ return f(std::forward<T>(t), std::forward<Arg>(a)); }

template<typename F, typename T, typename Head, typename... Args>
auto fold(F f, T&& init, Head&& h, Args&&... args) 
  -> decltype(f(std::forward<T>(init), std::forward<Head>(h)))
{ 
  return fold(f, f(std::forward<T>(init), std::forward<Head>(h)), 
              std::forward<Args>(args)...); 
}

// hack in a fold for void functions
struct ignore {};

// cannot be a lambda, needs to be polymorphic on the iterator type
struct end_or {
  template<typename InputIterator>
  bool operator()(bool in, const std::pair<InputIterator, InputIterator>& p) 
    { return in || p.first == p.second; }
};

// same same but different
struct inc {
  template<typename InputIterator>
  ignore operator()(ignore, std::pair<InputIterator, InputIterator>& p) 
    { p.first++; return ignore(); }
};

template<typename Fun, typename OutputIterator, 
         typename... InputIterators>
void zipWith(Fun f, OutputIterator out, 
             std::pair<InputIterators, InputIterators>... inputs) {
  if(fold(end_or(), false, inputs...)) return;
  while(!fold(end_or(), false, inputs...)) {
    *out++ = f( *(inputs.first)... );
    fold(inc(), ignore(), inputs...);
  }
}

template<typename Fun, typename OutputIterator, 
         typename InputIterator, typename... Rest>
void transformV(Fun f, OutputIterator out, InputIterator begin, InputIterator end,
                Rest... rest) 
{
  if(begin == end) return ;
  while(begin != end) {
    *out++ = f(*begin, *(rest)... );
    fold(inc2(), ignore(), begin, rest...);
  }
}

struct ternary_plus {
  template<typename T, typename U, typename V>
  auto operator()(const T& t, const U& u, const V& v) 
    -> decltype( t + u + v) // common type? 
    { return t + u + v; }
};

int main()
{
  using namespace std;
  vector<int> a = {1, 2, 3}, b = {1, 2}, c = {1, 2, 3};
  vector<int> out;

  zipWith(ternary_plus(), back_inserter(out)
          , make_pair(begin(a), end(a))
          , make_pair(begin(b), end(b))
          , make_pair(begin(c), end(c)));

  transformV(ternary_plus(), back_inserter(out),
             begin(a), end(a), begin(b), begin(c));

  for(auto x : out) { 
    std::cout << x << std::endl;
  }

  return 0;
}

与以前的版本相比,这是一个稍微改进的版本。像每一个 好的程序应该,它首先定义一个左折。

它仍然无法解决成对打包的迭代器问题。

在stdlib术语中,此函数将被称为transform并且会 要求只指定一个序列的长度和 其他人至少要这么久。我在这里称它为transformV以避免 名字冲突。