2
\$\begingroup\$

Here are two sets of variadic C++ functions to do fast summing and averaging of arrays (plain & std::array) or argument sets, (re)written as part of my generic toolbox.

The sum functions are pretty generic, the averaging ones operate on floating point based types only (the integer counterparts are application specific).

Usage example:

std::array<volatile float, 5> arr1 { 1.1f, 1.2f, 1.3f, 1.4f, 1.5f };
volatile float arr2[] { 1.1f, 1.2f, 1.3f, 1.4f, 1.5f };

volatile float res1 = sum(arr1);
volatile float res2 = avg(arr2, sizeof(arr2)/sizeof(arr2[0]));
volatile auto res3 = avg(1.2f, 1.4f, 1.6);

Live demo at: https://godbolt.org/g/DMHv7Y

The sum functions:

#include <cstdint>
#include <utility>
#include <type_traits>
#include <array>

/**
 * Return the sum of all values in the provided `std::array`.
 * @param  arr std::array
 * @return     sum
 */
template <typename T, size_t S>
inline constexpr auto sum(const std::array<T, S> arr) {
    std::remove_cv_t<std::remove_reference_t<T>> t = 0;
    for (const auto v : arr) {
        t += v;
    }
    return t;
}

/**
 * Return the sum of all values in the provided plain array.
 * @param  arr Array
 * @param  l   length
 * @return     sum
 */
template <typename T, typename = typename std::enable_if<std::is_pointer<T>::value, void>::type>
inline constexpr auto sum(const T arr, size_t l) {
    std::remove_cv_t<
        std::remove_reference_t<
            decltype(arr[0])>> t = 0;
    for (size_t i = 0; i < l; i++) {
        t += arr[i];
    }
    return t;
}

/**
 * Return the sum of all values. Tries to use the common type for the return value.
 * @param  ts Arguments to sum
 * @return    sum
 */
template <typename... Ts,
          typename = typename std::enable_if<!std::is_pointer<std::common_type_t<Ts...>>::value, void>::type>
inline constexpr auto sum(Ts&&... ts) {
    std::common_type_t<Ts...> t = 0;
    for (const auto val : std::initializer_list<std::common_type_t<Ts...>>{ts...}) {
        t += val;
    }
    return t;
}

The averaging functions:

#include <cstdint>
#include <utility>
#include <type_traits>
#include <array>

/**
 * Return the average of all values in the provided `std::array`.
 * @param  arr std::array
 * @return     average
 */
template <typename T, size_t S, typename = typename std::enable_if<std::is_floating_point<T>::value, void>::type>
inline constexpr auto avg(const std::array<T, S> arr) {
    std::remove_cv_t<std::remove_reference_t<T>> t = 0;
    for (const auto v : arr) {
        t += v;
    }
    return t / S;
}

/**
 * Return the average of all values in the provided array.
 * @param  arr array
 * @param  l   length
 * @return     average
 */
template <typename T, typename = typename std::enable_if<std::is_pointer<T>::value && std::is_floating_point<std::remove_pointer_t<T>>::value, void>::type>
inline constexpr auto avg(const T arr, size_t l) {
    std::remove_cv_t<
        std::remove_reference_t<
            decltype(arr[0])>> t = 0;
    for (size_t i = 0; i < l; i++) {
        t += arr[i];
    }
    return t / l;
}

/**
 * Return the average of all arguments. Tries to use the common type for the return value.
 * @param  ts Arguments to sum
 * @return    average
 */
template <typename... Ts,
          typename = typename std::enable_if<!std::is_pointer<std::common_type_t<Ts...>>::value && std::is_floating_point<std::common_type_t<Ts...>>::value, void>::type>
inline constexpr auto avg(Ts&&... ts) {
    std::common_type_t<Ts...> t = 0;
    for (const auto val : std::initializer_list<std::common_type_t<Ts...>>{ts...}) {
        t += val;
    }
    return t / sizeof...(Ts);
}

The generic patterns here can of course be used to create all kinds of data processing tools. I'd love to know if/how I can improve them.

\$\endgroup\$
6
  • \$\begingroup\$ Why are you passing the std::arrayby copy ? \$\endgroup\$
    – Benoît
    Commented May 27, 2017 at 16:01
  • \$\begingroup\$ Hey, thanks, good catch, that certainly should be a reference. I'll update it. \$\endgroup\$
    – Stefan
    Commented May 27, 2017 at 16:04
  • \$\begingroup\$ For arrays, i.e not pointer, you could have a template accepting const T (&arr)[S]. But at this pointer, you may just go with iterators to cover all cases. \$\endgroup\$
    – Benoît
    Commented May 27, 2017 at 16:15
  • \$\begingroup\$ Things just got interesting... I've changed it back and forth a few times and doing a (micro) benchmark shows that pass by copy (eg, shown above) is considerably faster (nearly as fast as a plain array pointer), pass by reference turning out 50% slower by comparison. \$\endgroup\$
    – Stefan
    Commented May 27, 2017 at 16:18
  • \$\begingroup\$ @Stefan, checkout this post. I recommend using something like -O3 -march=native when benchmarking. \$\endgroup\$ Commented May 27, 2017 at 16:46

1 Answer 1

3
\$\begingroup\$

Different approach:

I'd use iterators and standard library. Using them it becomes really simple:

template <typename ForwardIt, typename T = typename std::iterator_traits<ForwardIt>::value_type>
T average(ForwardIt first, ForwardIt last, T initvalue = {}) //will be 0 for builtins
{
    auto distance = std::distance(first, last);
    auto result = std::accumulate(first, last, initvalue);

    return result / distance;
}

It is also possible to have a distance argument that will be defaulted to what std::distance() produces, but since my use cases are mostly bound to RandomAccessIterators, I don't care, since distance is constant time on random access iterators.

It can be noticed that iterators abstract container type, so they become really easy to use.

The code actually is part of my library. It is so ancient that I forgot it even exists.

As for sum, it is simply using std::accumulate().

Some more tips:

Standard library has type traits. They help to debug templates. The toolbox should try to provide them as well.

template <typename T>
struct is_floating_point_array :
                public std::false_type 
{};

template <typename T, std::size_t size>
struct is_floating_point_array<T[size]> :
                public std::bool_constant<std::is_floating_point_v<T>>                    
{};

//one for std::array

Ironically, std::is_array<> won't work on instantiations of std::array, so I recommend writing your own. The above will not only provide easier debugging experience, but will also allow tagged dispatch or if constexpr opportunities. Even more, it will provide concept support, which will make metaprogramming even easier. I didn't directly inherit from std::bool_constant on the first phase since I favored readability to less code height, maybe I was wrong.

Iterators are essential part of standard library. I recommend using them at least in obvious cases. Currently using the code one needs to use std::size() or write one themselves. Pretty much everyone I know doesn't know about std::size(). std::begin() and std::end() (or their member counterparts) destroy opportunity for making the mistakes.

\$\endgroup\$
1
  • \$\begingroup\$ Small copy paste error: is_floating_point_array<T[N]> should be is_floating_point_array<T[size]> as there is no template parameter N. \$\endgroup\$ Commented May 27, 2017 at 18:39

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.