How to avoid an explicit loop when pulling out a vector of a particular member from a vector of a class containing that member

Viewed 136

Suppose I have a class

struct Foo {double x; double y;}

and std::vector<Foo> xy;

I also have a function to pull out the x members:

std::vector<double> bar(const std::vector<Foo>& xy);

which loops through each element of xy.

Is there a way of using the C++ standard library so I can avoid an explicit for loop?

3 Answers

You can use std::transform. E.g.

std::vector<double> bar(const std::vector<Foo>& xy) {
    std::vector<double> rt;
    rt.reserve(xy.size());
    std::transform(std::begin(xy), 
                   std::end(xy), 
                   std::back_inserter(rt), 
                   [](const Foo& f) { return f.x; });
    return rt;
}

For comparison here's the version using range-based for loop. As @463035818_is_not_a_number commented it needs less typing.

std::vector<double> foo(const std::vector<Foo>& xy) {
    std::vector<double> rt;
    rt.reserve(xy.size());
    for (auto const & f : xy) rt.push_back(f.x);
    return rt;
}

Using C++20's ranges offers a somewhat briefer alternative to std::transform:

std::vector<double> filtered;
filtered.reserve(xy.size());
std::ranges::copy(
  std::ranges::views::transform(xy, &Foo::x),
  std::back_inserter(filtered));

Or, as pointed out by @StoryTeller, as an alternative and even briefer approach to the init-empty-and-back-insert, use a view to directly intitialize the filtered vector (an approach that would allow it to be const):

auto view = xy | std::ranges::views::transform(&Foo::x);
std::vector<double> const filtered(view.begin(), view.end());

Well, behind the scenes there obviously has to be a loop somewhere. So it seems to me that the STL included for_each for this exact type of problem.

We can write the Bar function like so:

#include <algorithm>

auto Bar (const std::vector<Foo> & xy)
{
    std::vector<double> bar;
    bar.reserve(xy.size());
    auto extract_x = [&](const Foo & f){bar.push_back(f.x);};
    std::for_each(xy.begin(), xy.end(), extract_x);
    return bar;
}

Demos:


I think it's a good idea to prepare a few generic function wrappers of your own to avoid all those begin/ends. eg:

template <typename Containter, typename Functor>
void ForIndices (const Containter & c, Functor && f)
{
    for (size_t i = 0; i < c.size(); ++i)
        f(i);
}

Use like so:

auto Bar (const std::vector<Foo> & xy)
{
    std::vector<double> bar(xy.size());
    ForIndices(xy, [&](int i){bar[i] = xy[i].x;});
    return bar;
}

Demo

Related