有可能在c++中编写一个通用的可变zipWith吗?< / h1 >

Is it possible to write a generic variadic zipWith in C++?

本文关键字:zipWith lt gt h1 c++ 一个 有可能      更新时间:2023-10-16

我想要一个通用的变量的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) {
???
}

我有一个很长的答案,然后我改变了主意,使解决方案更短。但我将展示我的思考过程,并给出两个答案!

我的第一步是确定正确的签名。我不完全理解它,但是您可以将参数包视为隐藏文本转储的以逗号分隔的实际项列表。您可以在任意一侧通过更多逗号分隔的项目扩展列表!直接应用

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

你必须在表达式部分的参数包后面放一个"…"来查看展开的列表。您必须在常规参数部分也放一个:

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左值引用。此外,我们可以使用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对象。所以我要把它包起来。也许我应该在这里问一下....)

此时,我重新加载了页面,看到了一个响应(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值重载就不会被错误地选择。改变每个元组元素的部分的函数必须显式地采用当前索引。参数包剥离时,索引向上移动1:

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... );
}

表和最短的输入向量一样长,所以我们必须在当前输入向量首先结束时修剪表。(我希望我可以在for块中将fe标记为const。)我最初将firstrest作为std::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-限定符来解引用元素,并且函数可以使用它来进行重载解析。内部确定结果类型的递归"调用"需要使用std::forward来防止任何"降级"到左值引用。它还揭示了这个迭代中的一个缺陷:I必须提供返回类型

我会解决这个问题,但首先我想解释一下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函数向vector返回一个特殊的输出迭代器,该vector通过push_back成员函数添加元素。

以下是我拼凑的内容:

#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;
}

这是一个比以前版本稍微改进的变体。因为每个好的程序应该从定义左折开始。

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

在标准库术语中,该函数将被称为transform,并将要求只指定一个序列的长度,并且其他的至少要这么长。我在这里叫它transformV是为了避免名称冲突。