C++迭代结构体成员

7

假设我有一个结构体:

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

以及一个向量

std::vector<Boundary> boundaries;

最符合C++风格的方式是如何访问结构体并分别获取topleftbottomright的总和?

我可以编写一个类似于下面的循环:

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

这看起来有点重复。当然,我可以这样做:

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

但那样我就失去了所有有意义的结构体成员名称。有没有办法让我编写以下类似的函数:

sum_top=make_sum(boundaries,"top");

在C++中似乎没有反射(Reflection)这个选项。我可以使用C++版本14。


2
你可以为结构体定义运算符,这样你就不需要循环遍历成员了。 - Mansoor
由于结构体的所有成员都是相同类型,只需使用相同类型的指针并将其指向顶部,然后递增直到指针是&right的地址。 - SPlatten
3
@SPlatten...并且免费获得UB,因为您无法保证它们是连续的 :) - Quentin
1
@Quentin,结构体的内存布局将是连续的,因为每个整数都有特定的大小,并且一个会跟随另一个。 - SPlatten
1
@SPlatten 这是它的正式定义:https://dev59.com/WlkR5IYBdhLWcg3w9RoO - NathanOliver
显示剩余11条评论
4个回答

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

(我记得在C++17中,边界常量&'s可以使用auto)

这并没有使它适用于特定的元素,确实由于缺乏反射而不易于泛化。

然而,有一些方法可以缓解您的痛苦;

您可以使用指向成员的指针,这对于您的情况来说是可以的,但不太符合C++的风格:

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

(针对成员指针,请参见此处:类数据成员的指针 "::*"
您还可以将其更加通用化(任何容器,任何成员类型),并且还可以使用lambda表达式替换成员指针(也允许成员函数)。

4

我可能有点晚了,但是我想要添加一个答案,灵感来自@TobiasRibizel的回答。我们不需要在你的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';
}

它适用于C++14,在C++11上,constexpr函数默认都是const的,所以它们不能工作,但只需去掉constexpr就可以解决问题。好处在于您可以选择一些struct成员并对其进行迭代。如果您始终要迭代相同的几个成员,则可以添加using。这就是为什么我选择将指向成员的指针作为模板的一部分,即使实际上并不需要,因为我认为只有相同成员上的迭代器才应该是相同类型。
您还可以保持原样,将std::array替换为std::vector,并在运行时选择要迭代哪些成员。

4

你可以通过使用Boost Hana反射来实现所需的效果:

#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';
}

请参阅用户定义类型的内省,获取更多详细信息。

3

不需要深入了解C++对象的内存布局,我建议用“引用getter”替换成员变量,这会向结构体添加一些样板代码,但除了将top替换为top()外,不需要更改使用结构体成员的方式。

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];
   }
 }

这将返回单个边界的上、左、下、右部分,但似乎不是提问者想要的。 - user253751
@user253751 感谢您的注意,我已经更新了答案。 - Tobias Ribizel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接