7

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.

Amelse Etomer
  • 1,253
  • 10
  • 28
  • 2
    You could define the operators for the struct, so you don't need to loop over members. – Mansoor Dec 05 '19 at 13:17
  • Since all the members of the structure are the same type, just use a pointer of the same type and point it to top, then increment until the pointer is the address of &right. – SPlatten Dec 05 '19 at 13:18
  • 3
    @SPlatten ... and get UB free of charge because you have no guarantee that they are contiguous :) – Quentin Dec 05 '19 at 13:18
  • The answers to this question show a way how to iterate over member variables with boost: https://stackoverflow.com/questions/11031062/c-preprocessor-avoid-code-repetition-of-member-variable-list – Devon Cornwall Dec 05 '19 at 13:18
  • 1
    @Quentin, the memory layout of the structure will be contiguous in that each integer is a specific size and one will follow another. – SPlatten Dec 05 '19 at 13:19
  • @Quentin, what is UB? – Amelse Etomer Dec 05 '19 at 13:21
  • @SPlatten That is what should happen, it is not guaranteed. The compiler is allowed to add padding between any of the elements in the struct. – NathanOliver Dec 05 '19 at 13:22
  • @AmelseEtomer UB == Undefined Behavior. Basically it means your program could work, it could also not work, you have no way to know. – NathanOliver Dec 05 '19 at 13:23
  • @AmelseEtomer undefined behaviour. C++ does not guarantee that a class can be accessed like an array as SPlatten suggests, so trying to do so falls out of legal C++. – Quentin Dec 05 '19 at 13:23
  • @NathanOliver-ReinstateMonica, in all my years of coding I've never seen a compiler change the layout of a structure in this way. If you want to make certain it hasn't been tampered with then use sizeof(struct Boundary) / sizeof(int). – SPlatten Dec 05 '19 at 13:24
  • @Quentin, thats kind of like saying an array of chars could be modified by the compiler, that does not happen! – SPlatten Dec 05 '19 at 13:25
  • @SPlatten It's still undefined behavior. You don't have a pointer to an array so you can't treat it like one. Once you introduce UB into the code all bets are off as it can effect code that isn't even related. You just shouldn't do it. – NathanOliver Dec 05 '19 at 13:25
  • @NathanOliver-ReinstateMonica, I just don't believe it...I've never seen it happen, the size of a structure is always defined by its contents. – SPlatten Dec 05 '19 at 13:27
  • @SPlatten: [case in point](https://wandbox.org/permlink/cgIIWFJaAfvxcoio). The compiler inserted padding. If all the types are the same, then a *sane* compiler would not insert padding, and I've also never seen one do it. However, just because they don't doesn't mean it's okay. The point is that the Standard specifically allows compilers to insert padding if they want, so any assumption of contiguity is non-portable. – AndyG Dec 05 '19 at 13:35
  • 1
    @SPlatten Here is the canonical on it: https://stackoverflow.com/questions/40590216/is-it-legal-to-index-into-a-struct – NathanOliver Dec 05 '19 at 13:37
  • [This talk](https://youtu.be/abdeAew3gmQ) isaybe interesting for you. – n314159 Dec 08 '19 at 15:31

4 Answers4

5
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)

peterchen
  • 40,917
  • 20
  • 104
  • 186
4

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.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
4

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.

n314159
  • 4,990
  • 1
  • 5
  • 20
3

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];
   }
 }
Tobias Ribizel
  • 5,331
  • 1
  • 18
  • 33