0

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

template <typename Type>
Type add(const Type& a, const Type& b) 
{
    // some logic
    if(!((b >= 0) && (a > std::numeric_limits<T>::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 <typename... Args>
idk_how_type_need add_n_elements(Args... args)
{
    return //here i want smth like -> add(add(arg0, arg1), add(arg2, arg3)...);
}

it real? Or maybe there are some alternatives?

9
  • 1
    add(add(arg0, arg1), add(arg2, arg3)) should be the same as add(add(add(add(arg0, arg1), arg2), arg3)). You just need to get that type of syntax out of a fold expression. Commented Mar 9, 2023 at 17:31
  • @NathanOliver I'm just starting to learn templates, could you give an example of how to do this? Commented Mar 9, 2023 at 17:39
  • Your single add function doesn't appear meaningful – returning the sum as boolean can only yield 1 or 0 anyway, the check would require a large a for a small b to work at all. Worse: you don't return anything if the test fails (undefined behaviour!). Commented Mar 9, 2023 at 17:41
  • How would you want to handle an odd number of arguments? Commented Mar 9, 2023 at 17:43
  • @Aconcagua sorry, return bool - it incorrect, i edit Commented Mar 9, 2023 at 17:45

3 Answers 3

3

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 <concepts> // for optional C++20 concept line
#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> && ...) // optional C++20 concept line
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

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

#include <concepts> // for optional C++20 concept line
#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> // optional C++20 concept line
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';
}

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

#include <iostream>

template <typename ... T>
auto accumulate(const T& ... args) {
    return (args + ...); // or (... + 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';
}

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

Sign up to request clarification or add additional context in comments.

Comments

0

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 <iostream>

template <typename...Args>     // Unary right fold
auto sum1(Args ... args) {
    return (args + ...);
}
int main() {
    std::cout << sum1(1, 2, 3, 4) << '\n';
}

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 <iostream>
#include <tuple>

template <typename...Args>     // Unary right fold
auto sum1(Args ... args) {
    return (args + ...);
}
template <typename...Args>     // Unary left fold
auto sum2(Args ... args) {
    return (... + args);
}
template <typename...Args>     // Binary right fold
auto sum3(Args ... args) {
    std::tuple_element_t<0, std::tuple<Args...>> sum{};
    sum = (args + ... + sum);
    return sum;
}
template <typename...Args>     // Binary left fold
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';
}

.


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 <typename...Args>
auto sum5(Args ... args) {
    std::tuple_element_t<0, std::tuple<Args...>> sum{};

    // comma lhs: Lambda     comma   rhs
    ([&]{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 <iostream>
#include <tuple>
#include <utility>
#include <limits>
#include <iomanip>

template <typename...Args>
using MyType = std::tuple_element_t<0, std::tuple<Args...>>;

template <typename...Args>
std::pair<bool, MyType<Args...>> sumsWithOverflowCheck(Args ... args) {
    bool overflow{};
    MyType <Args...>sum{};

    ([&]    // Lambda Capture
        {   // Lambda Body
            const MyType<Args...> maxDelta{ std::numeric_limits<MyType<Args...>>::max() - sum };
            if (args > 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& [overflow, sum] = sumsWithOverflowCheck(1, 2, 3, 4);
        std::cout << "\nSum: " << std::setw(12) << sum << "\tOverflow: " << std::boolalpha << overflow << '\n';
    }
    {   //Test 2
        const auto& [overflow, sum] = sumsWithOverflowCheck(1,2,(std::numeric_limits<int>::max()-10),  20, 30);
        std::cout << "Sum: " << std::setw(12) << sum << "\tOverflow: " << overflow << '\n';
    }
}

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

2 Comments

If I understand you correctly, you are criticizing that my solution does not allow you to call arbitrary functions as a convolution operation. I have adapted my answer so that you can explicitly write the called function as part of the main call. Thanks for input! However, your answer doesn't currently allow that either. Fold expressions only work for operators, but the questioner explicitly wants to call an arbitrary function with "some logic". If you really only want to call operator+, a Fold expression is the correct answer. I have also added that to my post.
For those, who cannot see it: My comment reffered to the orignial answer. Here you just showed the reduce with std::initializer_list and std::reduce which is a clear overkill for this use case. Now you edited your answer amd added the fold expressions that I mentioned from the beginning. This explanation is to make it clear for people. BTW. The comma operator with unary right fold can be used for any function. But, cool down and have fun. Who cares?
-1

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 <iostream>
#include <numeric>    

//original function
template <typename Type>
Type add(const Type& a, const Type& b) 
{
    // some logic
    if(!((b >= 0) && (a > std::numeric_limits<Type>::max() - b)))
        return a + b;        
    throw std::runtime_error("some error");
}

//overloaded function
template <typename Type, typename ...Type2>
Type add(const Type& a, const Type& b, const Type2 & ...other)
{
    return add(add(a,b), other...); //calls itself if num of params > 2, or original "add" otherwise
}

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

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.