Jon McClung
Jon McClung

Reputation: 1704

Is it possible to have a templated, single-parameter sum function in C++?

In python, we can do this:

int_list = [1, 2, 3, 4, 5]
print(sum(int_list)) # prints 15
float_tuple = (1.2, 3.4, 9.9)
print(sum(float_tuple)) # prints 14.5

The sum function takes any iterable of elements that know how to be added to each other and to 0 and produces the sum.

I wanted to make an identical function in C++11. I'm aware that there exists the accumulate method, but I want a function that takes a single parameter. Essentially, I want to know how to make the following code compile:

#include <string>
#include <iostream>
#include <vector>
#include <deque>
#include <list>

template<typename iterable>
auto sum(iterable iterable_) {
    auto it = iterable_.begin();
    auto end = iterable_.end();
    if (it == end) {
        return 0;
    }
    auto res = *(it++); 
    while (it != end) {
        res += *it++;
    }
    return res;
}

int main() {
    std::vector<int> int_vector = {0, 1, 2, 3, 4, 5}; 
    std::cout << sum(int_vector) << '\n';    // prints 15
    std::deque<int> int_deque = {4, 5, 7};
    std::cout << sum(int_deque) << '\n';     // prints 16
    std::list<float> float_list = {1.2, 3.4, 9.9};
    std::cout << sum(float_list) << '\n';    // should print 14.5, but produces error.

}

This code almost works. The issue is that auto sees the return 0; in the case that the iterable is empty and it assumes that the function must return an int. Then it sees that the float version returns a float and it gets confused. Is there any way to tell the compiler to, say return float(0) if it sees that the return later on returns float?

Upvotes: 2

Views: 184

Answers (3)

Barry
Barry

Reputation: 303377

If you want something to work with any C++11 range (that is, anything that you can iterate over in a range-based for expression - including arrays as well as containers that have free begin and end), we can just add some using-declarations and wrap std::accumulate

template <class Range>
auto sum(Range&& range) {
    using std::begin;
    using std::end;
    using T = std::decay_t<decltype(*begin(range))>;

    return std::accumulate(begin(range), end(range), T{});
}

If you don't want to wrap accumulate, then you can just re-implement that loop as well to do the same thing.

Upvotes: 5

Sam Varshavchik
Sam Varshavchik

Reputation: 118445

The following approach works even with non-standard containers; as long as something implements begin() and end(), in a reasonable way.

#include <list>
#include <iostream>
#include <type_traits>

template<typename iterable>
auto sum(iterable && iterable_) {
    auto it = iterable_.begin();
    auto end = iterable_.end();

    typedef typename std::remove_reference<decltype(*it)>::type value_type;

    if (it == end) {
        return value_type(0);
    }
    auto res = *(it++);
    while (it != end) {
        res += *it++;
    }
    return res;
}

int main() {
    std::list<float> float_list = {1.2, 3.4, 9.9};
    std::cout << sum(float_list) << '\n';    // works now.
}

Upvotes: 2

Jerry Coffin
Jerry Coffin

Reputation: 490358

Yes, you can make this work at least for standard containers.

A standard container defines a type alias named value_type for the type of value stored in that container. For an empty container, you can return a value-constructed object of this type:

template<typename iterable>
auto sum(iterable const &iterable_) {
    auto it = iterable_.begin();
    auto end = iterable_.end();
    if (it == end) {
        return typename iterable::value_type();
    }
    auto res = *(it++); 
    while (it != end) {
        res += *it++;
    }
    return res;
}

This does depend on the contained type being default-constructible, but that's probably not a major problem (certainly works for primitive types like int and float).

Upvotes: 5

Related Questions