调用add函数,并对每两个后续参数执行一次。

huangapple go评论109阅读模式
英文:

Сall the add function on every next 2 arguments

问题

template
Type add(const Type& a, const Type& b)
{
// some logic
if (!((b >= 0) && (a > std::numeric_limits::max() - b))
return a + b;
}

template <typename... Args>
idk_how_type_need add_n_elements(Args... args)
{
return // here i want smth like -> add(add(arg0, arg1), add(arg2, arg3)...);
}


```plaintext
我有一个执行两个元素上的一些算术操作的函数,如下所示:

template <typename Type>
Type add(const Type& a, const Type& b) 
{
    // 一些逻辑
    if (!((b >= 0) && (a > std::numeric_limits<T>::max() - b))
        return a + b;
}

我想编写另一个模板函数,以相同的方式进行一些逻辑处理,接受N个参数,并对所有这些参数应用之前的函数,例如:

template <typename... Args>
idk_how_type_need add_n_elements(Args... args)
{
    return // 这里我想要类似于 -> add(add(arg0, arg1), add(arg2, arg3)...);
}

"template "和"template <typename... Args>"不需要翻译。

英文:

I have a function that does some arithmetic on two elements like this:

template &lt;typename Type&gt;
Type add(const Type&amp; a, const Type&amp; b) 
{
    // some logic
    if(!((b &gt;= 0) &amp;&amp; (a &gt; std::numeric_limits&lt;T&gt;::max() - b))
        return a + b;
}

I want to write another template function that will do some logic in the same way, take N arguments and apply the previous function on all these arguments, for example:

template &lt;typename... Args&gt;
idk_how_type_need add_n_elements(Args... args)
{
    return //here i want smth like -&gt; add(add(arg0, arg1), add(arg2, arg3)...);
}

it real? Or maybe there are some alternatives?

答案1

得分: 3

你想要的可以通过在std::initializer_list上调用std::reduce来实现。

**注意:**这个解决方案只在被调用的函数是可交换的情况下有效,即如果add(a, b) == add(b, a)。你可以用std::accumulate来替代std::reduce以保证顺序add(add(add(a, b), c), d) ...。如果你真的需要add(add(a, b), add(c, d)) ...,这个解决方案将不起作用!

#include <concepts> // 用于可选的C++20概念行
#include <iostream>
#include <numeric>

template <typename T>
T add(const T& a, const T& b) {
    return a + b;
}

template <typename T, typename ... U>
requires (std::same_as<T, U> && ...) // 可选的C++20概念行
T reduce(const T& first, const U& ... args) {
    auto const list = std::initializer_list<T>{args ...};
    return std::reduce(list.begin(), list.end(), first, add<T>);
}

int main() {
    std::cout << reduce(1) << '\n';
    std::cout << reduce(1, 2) << '\n';
    std::cout << reduce(1, 2, 3) << '\n';
    std::cout << reduce(1, 2, 3, 4) << '\n';
    std::cout << reduce(1, 2, 3, 4, 5) << '\n';
}
1
3
6
10
15

如果你想要让你的add函数成为接口的一部分,你可以将它更改为:

#include <concepts> // 用于可选的C++20概念行
#include <iostream>
#include <numeric>

template <typename T>
T add(const T& a, const T& b) {
    return a + b;
}

template <typename Fn, typename T, typename ... U>
requires (std::same_as<T, U> && ...) && std::invocable<Fn, T, T> // 可选的C++20概念行
T reduce(Fn&& fn, const T& first, const U& ... args) {
    auto const list = std::initializer_list<T>{args ...};
    return std::reduce(list.begin(), list.end(), first, std::forward<Fn>(fn));
}

int main() {
    std::cout << reduce(add<int>, 1) << '\n';
    std::cout << reduce(add<int>, 1, 2) << '\n';
    std::cout << reduce(add<int>, 1, 2, 3) << '\n';
    std::cout << reduce(add<int>, 1, 2, 3, 4) << '\n';
    std::cout << reduce(add<int>, 1, 2, 3, 4, 5) << '\n';
}

如果你的add函数只是一个operator+(或者另一个二元操作符),你可以使用C++17的Fold Expression

#include <iostream>

template <typename ... T>
auto accumulate(const T& ... args) {
    return (args + ...); // 或者 (... + args)
}

int main() {
    std::cout << accumulate(1) << '\n';
    std::cout << accumulate(1, 2) << '\n';
    std::cout << accumulate(1, 2, 3) << '\n';
    std::cout << accumulate(1, 2, 3, 4) << '\n';
    std::cout << accumulate(1, 2, 3, 4, 5) << '\n';
}

注意,(args + ...)表示... (a + (b + (c + d)),而(... + args)表示(((a + b) + c) + d) ...

英文:

What you want can be done by calling std::reduce on an std::initializer_list.

Attention: This solution works only if the called function is commutative, i.e. if add(a, b) == add(b, a). You may replace std::reduce by std::accumulate for the guaranteed order add(add(add(a, b), c), d) .... If you really need add(add(a, b), add(c, d)) ..., this solution will not work!

#include &lt;concepts&gt; // for optional C++20 concept line
#include &lt;iostream&gt;
#include &lt;numeric&gt;

template &lt;typename T&gt;
T add(const T&amp; a, const T&amp; b) {
    return a + b;
}

template &lt;typename T, typename ... U&gt;
requires (std::same_as&lt;T, U&gt; &amp;&amp; ...) // optional C++20 concept line
T reduce(const T&amp; first, const U&amp; ... args) {
    auto const list = std::initializer_list&lt;T&gt;{args ...};
    return std::reduce(list.begin(), list.end(), first, add&lt;T&gt;);
}

int main() {
    std::cout &lt;&lt; reduce(1) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(1, 2) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(1, 2, 3) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(1, 2, 3, 4, 5) &lt;&lt; &#39;\n&#39;;
}

1
3
6
10
15

If you want to make your called add function part of the interface you can change this to:

#include &lt;concepts&gt; // for optional C++20 concept line
#include &lt;iostream&gt;
#include &lt;numeric&gt;

template &lt;typename T&gt;
T add(const T&amp; a, const T&amp; b) {
    return a + b;
}

template &lt;typename Fn, typename T, typename ... U&gt;
requires (std::same_as&lt;T, U&gt; &amp;&amp; ...) &amp;&amp; std::invocable&lt;Fn, T, T&gt; // optional C++20 concept line
T reduce(Fn&amp;&amp; fn, const T&amp; first, const U&amp; ... args) {
    auto const list = std::initializer_list&lt;T&gt;{args ...};
    return std::reduce(list.begin(), list.end(), first, std::forward&lt;Fn&gt;(fn));
}

int main() {
    std::cout &lt;&lt; reduce(add&lt;int&gt;, 1) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(add&lt;int&gt;, 1, 2) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(add&lt;int&gt;, 1, 2, 3) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(add&lt;int&gt;, 1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; reduce(add&lt;int&gt;, 1, 2, 3, 4, 5) &lt;&lt; &#39;\n&#39;;
}

If your add function is simply an operator+ (or another binary operator), you can use a C++17 Fold Expression instead.

#include &lt;iostream&gt;

template &lt;typename ... T&gt;
auto accumulate(const T&amp; ... args) {
    return (args + ...); // or (... + args)
}

int main() {
    std::cout &lt;&lt; accumulate(1) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; accumulate(1, 2) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; accumulate(1, 2, 3) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; accumulate(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; accumulate(1, 2, 3, 4, 5) &lt;&lt; &#39;\n&#39;;
}

Note that (args + ...) means ... (a + (b + (c + d)) and (... + args) means (((a + b) + c) + d) ....

答案2

得分: 0

嗯,Benjamin Buch的回答对于这个用例来说可能有点过于复杂,而且并没有完全解决提问者的问题,因为他想对他的N个参数应用一个函数。

关于第一点:对于添加几个值,我们不需要std::reduce。因此,把值首先放入容器中,这里是一个std::initializer_list,然后对这个容器进行归约,对于添加值来说就太复杂了。如果你想要并行化或矢量化大数据,使用可交换和可结合的运算符,std::reduce有很大的优势。惯用的首选解决方案是使用fold expressions。我将在下面解释这一点。

关于第二点:这一点根本没有被解决。我将在下面展示一种基于fold表达式的解决方案。


使用fold表达式构建总和。如果你查看CPP参考这里,你可以看到如何轻松地归约参数包(在你的例子中是Args... args)。如果你在那里看一下,你会发现:

语法:

( pack op ... )        (1)     
( ... op pack )        (2)     
( pack op ... op init )     (3)     
( init op ... op pack )     (4)     

1) 一元右折叠
2) 一元左折叠
3) 二元右折叠
4) 二元左折叠

如果你用你的用例替换“pack”为“args”,“op”为“+”,那么它会变成这样:

( args + ... )
( ... + args )        
( args + ... + sum ) 
( sum + ... + args ) 

所有变体都有用例。但首先让我展示基于一元右折叠的最简单的解决方案:

#include <iostream>

template <typename...Args>     // 一元右折叠
auto sum1(Args ... args) {
    return (args + ...);
}
int main() {
    std::cout << sum1(1, 2, 3, 4) << '\n';
}

这确实非常简洁。

你可以在CPP参考中阅读这里会发生什么。

解释
折叠表达式的实例化展开表达式e如下:

  1. 一元右折叠(E op ...)变成(E1 op (... op (EN-1 op EN)))

对于我们上面的折叠表达式(args + ...),我们将得到以下结果:(1 + (2 + (3 + 4)))。对于“+”运算符,我们可以省略所有括号,最终得到:1 + 2 + 3 + 4

好了

请看下面的代码片段,我们在其中使用了fold表达式的所有4个变体:

#include <iostream>
#include <tuple>

template <typename...Args>     // 一元右折叠
auto sum1(Args ... args) {
    return (args + ...);
}
template <typename...Args>     // 一元左折叠
auto sum2(Args ... args) {
    return (... + args);
}
template <typename...Args>     // 二元右折叠
auto sum3(Args ... args) {
    std::tuple_element_t<0, std::tuple<Args...>> sum{};
    sum = (args + ... + sum);
    return sum;
}
template <typename...Args>     // 二元左折叠
auto sum4(Args ... args) {
    std::tuple_element_t<0, std::tuple<Args...>> sum{};
    sum = (sum + ... + args);
    return sum;
}
int main() {
    std::cout << sum1(1, 2, 3, 4) << '\n';
    std::cout << sum2(1, 2, 3, 4) << '\n';
    std::cout << sum3(1, 2, 3, 4) << '\n';
    std::cout << sum4(1, 2, 3, 4) << '\n';
}


接下来,到第二部分。如何对参数包的每个参数应用一个函数。也有许多潜在的解决方案,但是,因为我正在解释fold表达式,我将展示一种对小函数(特别是lambda)使用fold表达式的解决方案。

这里解决方案的主要部分是使用“逗号”运算符与一元右折叠结合使用。

由于逗号运算符并不经常使用,让我们再次阅读一下这里

所以,lhs, rhs执行以下操作:

首先,评估左操作数lhs,并丢弃其结果值。
然后,发生一个序列点,以使lhs的所有副作用完成。
然后,评估右操作数rhs,并由逗号运算符作为非左值返回其结果。

这可以与一元右折叠很好地结合使用。

让我们以最简单的形式来看一个lambda[]{}。你只看到了捕获和主体。这是一个完全有效的Lambda。如果你想调用这个lambda,你可以简单地写成[]{} ()。这调用一个空lambda并什么也不做。只是为了演示:如果我们在总和示例中使用:

template <typename...Args>
auto sum5(Args ... args) {
    std::tuple_element_t<0, std::tuple<Args...>> sum{};

    // 逗号lhs: Lambda     逗号   rhs
    ([&]{sum += args;} ()      ,     ...);

    return sum;
}

然后会发生以下情况:

  • 我们定义一个lambda并通过引用捕获所有外部变量,这
英文:

Hm, the answer of user Benjamin Buch seems to be

  1. a little bit over-complicated for this use case
  2. and does not address the question of the OP fully, because he wanted to apply a function for his N arguments

Regarding 1.: For Just adding few values, we do not need std::reduce. So, putting the value first in a container, here in a std::initializer_list, and then reducing this container, is for adding values by far too complicated. std::reduce has big advantages, if you want to parallelize or vectorize big data using associative and commutative operators. The idiomatic preferred solution would be to use fold expressions. I will explain this below.

Regarding 2.: This is not addressed at all. I will show a solution, also based on fold expressions, below.


Building the sum with fold expressions. If you look in the CPP reference here, you can see, how parameter packs (in your example Args... args) can be easily reduced. If you look there, then you can read:

Syntax:

( pack op ... ) 	        (1) 	
( ... op pack ) 	        (2) 	
( pack op ... op init ) 	(3) 	
( init op ... op pack ) 	(4) 	

1) unary right fold
2) unary left fold
3) binary right fold
4) binary left fold

If you replace, for your uses case "pack" with "args" and "op" with "+", then it would read like that:

( args + ... )
( ... + args ) 	        
( args + ... + sum ) 
( sum + ... + args ) 

There are use case for all variants. But let me first show you the easiest solution based on an unary right fold:

#include &lt;iostream&gt;

template &lt;typename...Args&gt;     // Unary right fold
auto sum1(Args ... args) {
    return (args + ...);
}
int main() {
    std::cout &lt;&lt; sum1(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
}

And this is really very compact simple.

You can read in the CPP Reference, what will happen here.

> Explanation
> The instantiation of a fold expression expands the expression e as follows:
> 1) Unary right fold (E op ...) becomes (E1 op (... op (EN-1 op EN)))

For our above fold expression (args + ...) we would get the following:
(1 + ( 2 + (3 + 4))). For the '+' operator, we can omit all braces and finally get: 1 + 2 + 3 + 4.

Nice

Please see below a piece of code where we use all 4 variants of fold expressions:

#include &lt;iostream&gt;
#include &lt;tuple&gt;

template &lt;typename...Args&gt;     // Unary right fold
auto sum1(Args ... args) {
    return (args + ...);
}
template &lt;typename...Args&gt;     // Unary left fold
auto sum2(Args ... args) {
    return (... + args);
}
template &lt;typename...Args&gt;     // Binary right fold
auto sum3(Args ... args) {
    std::tuple_element_t&lt;0, std::tuple&lt;Args...&gt;&gt; sum{};
    sum = (args + ... + sum);
    return sum;
}
template &lt;typename...Args&gt;     // Binary left fold
auto sum4(Args ... args) {
    std::tuple_element_t&lt;0, std::tuple&lt;Args...&gt;&gt; sum{};
    sum = (sum + ... + args);
    return sum;
}
int main() {
    std::cout &lt;&lt; sum1(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; sum2(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; sum3(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; sum4(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
}

.


Next. To the second part. How to apply a function on each argument of a parameter pack. There are also many potential solutions, but, because I was explaining about fold expressions, I will show a solution for small functions, so, specifically lambdas, by using fold expressions.

And the main part of the solution here is to use the 'comma` operator in conjunction with unary right fold.

Since the comma-operator is not often used, lets read again about it here.

so, lhs, rhs does the following:

> First, the left operand, lhs, is evaluated and its result value is discarded.
> Then, a sequence point takes place, so that all side effects of lhs are complete.
> Then, the right operand, rhs, is evaluated and its result is returned by the comma operator as a non-lvalue.

And this can be fantastically combined with an unary right fold.

Lets take a lambda in its simplest form: []{}. You see just the capture and the body. This is a fully valid Lambda. If you want to call this lambda, then you can simply write []{} (). This calls an empty lambda and does nothing. Just for demonstration purposes: If we use for the sum example:

template &lt;typename...Args&gt;
auto sum5(Args ... args) {
    std::tuple_element_t&lt;0, std::tuple&lt;Args...&gt;&gt; sum{};

    // comma lhs: Lambda     comma   rhs
    ([&amp;]{sum += args;} ()      ,     ...);

    return sum;
}

then the follwoing will happen:

  • We define a lambda and capture all outer variables via reference, here especially sum
  • In the body we add args to the sum
  • Then we call the function with (). This is the "lhs" of the comma operator
  • Then we do a unary right fold and call the lambda again with the next args
  • and so on and so on.

If you analyze this step by step, you will understand it.

The next step is then easy. Of yourse can stuff more functionality in your lambda. And with that, you can apply a function on all arguments of a parameter pack.

This will be an example for your intended functionality:

#include &lt;iostream&gt;
#include &lt;tuple&gt;
#include &lt;utility&gt;
#include &lt;limits&gt;
#include &lt;iomanip&gt;

template &lt;typename...Args&gt;
using MyType = std::tuple_element_t&lt;0, std::tuple&lt;Args...&gt;&gt;;

template &lt;typename...Args&gt;
std::pair&lt;bool, MyType&lt;Args...&gt;&gt; sumsWithOverflowCheck(Args ... args) {
    bool overflow{};
    MyType &lt;Args...&gt;sum{};

    ([&amp;]    // Lambda Capture
        {   // Lambda Body
            const MyType&lt;Args...&gt; maxDelta{ std::numeric_limits&lt;MyType&lt;Args...&gt;&gt;::max() - sum };
            if (args &gt; maxDelta) overflow = true;
            sum += args; 
        }   // End of lambda body
        ()  // Call the Lampda
        , ...);  // unary right fold over comma operator

    return { overflow, sum };
}
int main() {
    {   // Test 1
        const auto&amp; [overflow, sum] = sumsWithOverflowCheck(1, 2, 3, 4);
        std::cout &lt;&lt; &quot;\nSum: &quot; &lt;&lt; std::setw(12) &lt;&lt; sum &lt;&lt; &quot;\tOverflow: &quot; &lt;&lt; std::boolalpha &lt;&lt; overflow &lt;&lt; &#39;\n&#39;;
    }
    {   //Test 2
        const auto&amp; [overflow, sum] = sumsWithOverflowCheck(1,2,(std::numeric_limits&lt;int&gt;::max()-10),  20, 30);
        std::cout &lt;&lt; &quot;Sum: &quot; &lt;&lt; std::setw(12) &lt;&lt; sum &lt;&lt; &quot;\tOverflow: &quot; &lt;&lt; overflow &lt;&lt; &#39;\n&#39;;
    }
}

Of course you would elimitate all unnecessary line breaks in the lambda, to make it compacter.

答案3

得分: -1

如所说,有很多方法可以实现这个。可能最简单的解决方案是添加一个处理3个或更多参数的重载函数,然后递归调用自己,如下所示:

#include <iostream>
#include <numeric>    

//原始函数
template <typename Type>
Type add(const Type& a, const Type& b) 
{
    // 一些逻辑
    if (!((b >= 0) && (a > std::numeric_limits<Type>::max() - b)))
        return a + b;        
    throw std::runtime_error("some error");
}

//重载函数
template <typename Type, typename ...Type2>
Type add(const Type& a, const Type& b, const Type2 & ...other)
{
    return add(add(a, b), other...); // 如果参数数目 > 2,则调用自身,否则调用原始的 "add" 函数
}

int main() {
    std::cout << add(1, 2) << '\n';
    std::cout << add(1, 2, 3) << '\n';
    std::cout << add(1, 2, 3, 4) << '\n';
    std::cout << add(1, 2, 3, 4, 5) << '\n';
    return 0;
}

所以,我只添加了一个简单易懂的一行函数,并解释了它的工作原理。通常情况下,通过将可变参数模板与递归结合使用,你可以更轻松地实现许多复杂的任务。

英文:

As it been said, there are many ways to do it. Probably the simplest solution, will be to add overloaded function that will handle 3 or more parameters and call itself recursively, like this:

#include &lt;iostream&gt;
#include &lt;numeric&gt;    
//original function
template &lt;typename Type&gt;
Type add(const Type&amp; a, const Type&amp; b) 
{
// some logic
if(!((b &gt;= 0) &amp;&amp; (a &gt; std::numeric_limits&lt;Type&gt;::max() - b)))
return a + b;        
throw std::runtime_error(&quot;some error&quot;);
}
//overloaded function
template &lt;typename Type, typename ...Type2&gt;
Type add(const Type&amp; a, const Type&amp; b, const Type2 &amp; ...other)
{
return add(add(a,b), other...); //calls itself if num of params &gt; 2, or original &quot;add&quot; otherwise
}
int main() {
std::cout &lt;&lt; add(1, 2) &lt;&lt; &#39;\n&#39;;
std::cout &lt;&lt; add(1, 2, 3) &lt;&lt; &#39;\n&#39;;
std::cout &lt;&lt; add(1, 2, 3, 4) &lt;&lt; &#39;\n&#39;;
std::cout &lt;&lt; add(1, 2, 3, 4, 5) &lt;&lt; &#39;\n&#39;;
return 0;
}

So, I only added a one-line function, that is easy to understand and explain how it works, and in general you can make a lot of complex stuffs much easier by combining variadic templates with recursion

huangapple
  • 本文由 发表于 2023年3月10日 01:23:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/75688029.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定