Amelse Etomer
Amelse Etomer

Reputation: 1253

C++ iterate over members of struct

Say I have a struct:

struct Boundary {
  int top;
  int left;
  int bottom;
  int right;
}

and a vector

std::vector<Boundary> boundaries;

What would be the most C++ style way to access the structs to get the sum of top, left, bottom and right separately?

I could write a loop like

for (auto boundary: boundaries) {
  sum_top+=boundary.top;
  sum_bottom+=boundary.bottom;
  ...
}

This seems like a lot of repetition. Of course I could do this instead:

std::vector<std::vector<int>> boundaries;

for (auto boundary: boundaries) {
  for(size_t i=0; i<boundary.size();i++) {
    sums.at(i)+=boundary.at(i)
  }
}

But then I'd loose all the meaningful struct member names. Is there a way so that I can write a something like the following function:

sum_top=make_sum(boundaries,"top");

Reflection does not seem to be an option in C++. I am open to use C++ up to Version 14.

Upvotes: 7

Views: 3132

Answers (4)

n314159
n314159

Reputation: 5095

I am probably a bit late to the party, but I wanted to add answer inspired by the one of @TobiasRibizel. Instead of adding much boilerplate code to your struct we add more boilerplate code once in the form of an iterator over (specified) members of a struct.

#include <iostream>
#include <string>
#include <map>

template<class C, typename T, T C::* ...members>
class struct_it {
public:
    using difference_type = std::ptrdiff_t;
    using value_type = T;
    using pointer = T*;
    using reference = T&;
    using iterator_category = std::bidirectional_iterator_tag;

    constexpr struct_it (C &c) : _index{0}, _c(c) 
    {}

    constexpr struct_it (size_t index, C &c) : _index{index}, _c(c) 
    {}

    constexpr static struct_it make_end(C &c) {
        return struct_it(sizeof...(members), c);
    }

    constexpr bool operator==(const struct_it& other) const {
        return other._index == _index;   // Does not check for other._c == _c, since that is not always possible. Maybe do &other._c == &_c?
    }

    constexpr bool operator!=(const struct_it& other) const {
        return !(other == *this);
    }

    constexpr T& operator*() const {
        return _c.*_members[_index];
    }

    constexpr T* operator->() const {
        return &(_c.*_members[_index]);
    }

    constexpr struct_it& operator--() {
        --_index;
        return *this;
    }

    constexpr struct_it& operator--(int) {
        auto copy = *this;
        --_index;
        return copy;
    }
    constexpr struct_it& operator++() {
        ++_index;
        return *this;
    }

    constexpr struct_it& operator++(int) {
        auto copy = *this;
        ++_index;
        return copy;
    }


private:
    size_t _index;
    C &_c;
    std::array<T C::*, sizeof...(members)> _members = {members...};  // Make constexpr static on C++17
};

template<class C, typename T, T C::* ...members>
using cstruct_it = struct_it<const C, T, members...>;

struct boundary {
    int top;
    int bottom;
    int left;
    int right;

    using iter = struct_it<boundary, int, &boundary::top, &boundary::bottom, &boundary::left, &boundary::right>;
    using citer = cstruct_it<boundary, int, &boundary::top, &boundary::bottom, &boundary::left, &boundary::right>;

    iter begin() {
        return iter{*this};
    }

    iter end() {
        return iter::make_end(*this);
    }

    citer cbegin() const {
        return citer{*this};
    }

    citer cend() const {
        return citer::make_end(*this);
    }
};


int main() {
    boundary b{1,2,3,4};

    for(auto i: b) {
        std::cout << i << ' '; // Prints 1 2 3 4
    }

    std::cout << '\n';
}

It works on C++14, on C++11 the constexpr functions are all const by default so they don't work, but just getting rid of the constexpr should do the trick. The nice thing is that you can choose just some members of your struct and iterate over them. If you have the same few members that you will always iterate over, you can just add a using. That is why I chose to make the pointer-to-members part of the template, even if it is actually not necessary, since I think that only the iterators over the same members should be of the same type.

One could also leave that be, replace the std::array by an std::vector and choose at runtime over which members to iterate.

Upvotes: 4

Tobias Ribizel
Tobias Ribizel

Reputation: 5421

Without going too much into the memory layout of C++ objects, I would propose replacing the members by 'reference-getters', which adds some boilerplate code to the struct, but except for replacing top by top() doesn't require any changes in the way you use the struct members.

struct Boundary {
   std::array<int, 4> coordinates;
   int& top() { return coordinates[0]; }
   const int& top() const { return coordinates[0]; }
   // ...
 }

 Boundary sum{};
 for (auto b : boundaries) {
   for (auto i = 0; i < 4; ++i) {
      sum.coordinates[i] += b.coordinates[i];
   }
 }

Upvotes: 3

Maxim Egorushkin
Maxim Egorushkin

Reputation: 136355

You can achieve the desired effect with Boost Hana reflection:

#include <iostream>
#include <vector>
#include <boost/hana.hpp>

struct Boundary {
  BOOST_HANA_DEFINE_STRUCT(Boundary,
      (int, top),
      (int, left),
      (int, bottom),
      (int, right)
  );
};

template<class C, class Name>
int make_sum(C const& c, Name name) {
    int sum = 0;
    for(auto const& elem : c) {
        auto& member = boost::hana::at_key(elem, name);
        sum += member;
    }
    return sum;
}

int main() {
    std::vector<Boundary> v{{0,0,1,1}, {1,1,2,2}};

    std::cout << make_sum(v, BOOST_HANA_STRING("top")) << '\n';
    std::cout << make_sum(v, BOOST_HANA_STRING("bottom")) << '\n';
}

See Introspecting user-defined types for more details.

Upvotes: 4

peterchen
peterchen

Reputation: 41106

std::accumulate(boundaries.begin(), boundaries.end(), 0, 
 [](Boundary const & a, Boundary const & b) { return a.top + b.top); });

(IIRC the Boundary const &'s can be auto'd in C++17)

This doesn't make it generic for the particular element, which - indeed, due to the lack of reflection - isn't easy to generalize.

There are a few ways to ease your pain, though;

You could use a pointer-to-member, which is fine for your szenario but not very c-plusplus-y:

int Sum(vector<Boundary>const & v, int Boundary::*pMember)
{
   return std::accumulate( /*...*/, 
     [&](Boundary const & a, Boundary const & b)
     {
        return a.*pMember + b.*pMember;
     });
}

int topSum = Sum(boundaries, &Boundary::top);

(For pointer-to-member, see e.g. here: Pointer to class data member "::*")

You could also make this generic (any container, any member type), and you could also replace the pointer-to-member with a lambda (also allowing member functions)

Upvotes: 5

Related Questions