C++: function that works with container and container of pointers as well

153 Views Asked by At

I think I'm facing something that I imagine is a quite common problem here. I'd like to write a function that would be able to accept both a container (let's say std::vector) of objects, and a container of pointers to those objects.

What would be the proper way to do so?

Right now, I'm thinking

int sum(std::vector<int *> v)
{
  int s = 0;
  for (int * i : v) s += *i;
  return s;
}

int sum(std::vector<int> v)
{
  std::vector<int *> vp;
  for (size_t i = 0; i < v.size(); ++i)
    vp[i] = &v[i];
  return sum(vp);
}

But it doesn't seem quite right, does it?

4

There are 4 best solutions below

2
463035818_is_not_an_ai On

Consider the standard algorithm library where the problem you see has a solution.

Most algorithms have some default behavior but often allow you to customize that behavior via functor parameters.

For your specific case the algorithm of choice is std::accumulate.

Because this algorithm already exists I can restrict to a rather simplified illustration here:

#include <iostream>
#include <functional>

template <typename T,typename R,typename F = std::plus<>>
R sum(const std::vector<T>& v,R init,F f = std::plus<>{})
{  
  for (auto& e : v) init = f(init,e);
  return init;
}

int main() {
    std::vector<int> x{1,2,3,4};
    std::vector<int*> y;
    for (auto& e : x ) y.push_back(&e);

    std::cout << sum(x,0)  << "\n";
    std::cout << sum(y,0,[](auto a, auto b) {return a + *b;});

}

std::plus is a functor that adds two values. Because the return type may differ from the vectors element type an additional template parameter R is used. Similar to std::accumulate this is deduced from the initial value passed as parameter. When adding int the default std::plus<> is fine. When adding integers pointed to by pointers, the functor can add the accumulator with the dereferenced vector element. As already mentioned this is just a simple toy example. In the above link you can find a possible implementation of std::accumulate (which uses iterators rather than the container directly).

0
mch On

You can make a function template, which behaves differently for pointer and non-pointer:

#include <iostream>
#include <vector>
using namespace std;

template <class T>
auto sum(const std::vector<T> &vec)
{
    if constexpr (std::is_pointer_v<T>)
    {
        typename std::remove_pointer<T>::type sum = 0;
        for (const auto & value : vec) sum += *value;
        return sum;
    }
    if constexpr (!std::is_pointer_v<T>)
    {
        T sum = 0;
        for (const auto & value : vec) sum += value;
        return sum;
    }
}

int main(){
    std::vector<int> a{3, 4, 5, 8, 10};
    std::vector<int*> b{&a[0], &a[1], &a[2], &a[3], &a[4]};
    cout << sum(a) << endl;
    cout << sum(b) << endl;
}

https://godbolt.org/z/sch3KovaK

You can move almost everything out of the if constexpr to reduce code duplication:

template <class T>
auto sum(const std::vector<T> &vec)
{
    typename std::remove_pointer<T>::type sum = 0;
    for (const auto & value : vec) 
    {
        if constexpr (std::is_pointer_v<T>)
            sum += *value;
        if constexpr (!std::is_pointer_v<T>)
            sum += value;
    }
    return sum;
}

https://godbolt.org/z/rvqK89sEK

0
Caleth On

With C++20 (or another ranges library), you can easily add or remove pointerness

template <std::ranges::range R, typename T>
concept range_of = requires std::same<std::ranges::range_value_t<R>, T>;

template <range_of<int *> IntPointers>
int sum_pointers(IntPointers int_pointers)
{
    int result = 0;
    for (int * p : int_pointers) result += *p;
    return result;
}

void call_adding_pointer()
{
    std::vector<int> v;
    sum_pointers(v | std::ranges::views::transform([](int & i){ return &i; });
}

Or

template <range_of<int> Ints>
int sum(Ints ints)
{
    int result = 0;
    for (int i : ints) result += i;
    return result;
}

void call_removing_pointer()
{
    std::vector<int *> v;
    sum(v | std::ranges::views::transform([](int * p){ return *p; });
}
7
AudioBubble On

Based on @mch solution:


template<typename T>
std::array<double, 3> center(const std::vector<T> & particles)
{
    if (particles.empty())
        return {0, 0, 0};

    std::array<double, 3> cumsum = {0, 0, 0};

    if constexpr (std::is_pointer_v<T>)
    {
        for (const auto p : particles)
        {
            cumsum[0] += p->getX();
            cumsum[1] += p->getY();
            cumsum[2] += p->getZ();
        }
    }
    if constexpr (not std::is_pointer_v<T>)
    {
        for (const auto p : particles)
        {
            cumsum[0] += p.getX();
            cumsum[1] += p.getY();
            cumsum[2] += p.getZ();
        }
    }
    double f = 1.0 / particles.size();
    cumsum[0] *= f;
    cumsum[1] *= f;
    cumsum[2] *= f;
    return cumsum;
}

Much cleaner and more efficient solution using std::invoke:

std::array<double, 3> centroid(const std::vector<T> & particles)
{
    if (particles.empty())
        return {0, 0, 0};

    std::array<double, 3> cumsum{0.0, 0.0, 0.0};
    for (auto && p : particles)
    {
        cumsum[0] += std::invoke(&topology::Particle::getX, p);
        cumsum[1] += std::invoke(&topology::Particle::getY, p);
        cumsum[2] += std::invoke(&topology::Particle::getZ, p);
    }

    double f = 1.0 / particles.size();
    cumsum[0] *= f;
    cumsum[1] *= f;
    cumsum[2] *= f;
    return cumsum;
}