按变量名遍历结构体

6

更新:6个月后,我刚刚发现了这个答案:Is it legal to index into a struct?: Answer by Slava。我认为这比这里提供的任何解决方案都要好得多,因为绝对没有未定义的行为。希望这能帮到下一个人,因为对我来说已经太晚实现了。


在您评论之前,告诉我使用数组或向量或任何形式的容器,这是一个残酷的事实,我不能这样做。我知道,这可以通过数组解决,除此之外的任何解决方案都相当"hacky"。我很乐意使用容器,但我绝对不可以。

我是一家非常大公司的中级开发人员,我们正在使用一个公司范围的库来通过以太网发送数据。有各种各样的原因,不能支持数组/向量,而是使用POD(Plain Old Data - chars、floats、ints、bools)结构体。我从一个floats数组开始,必须使用相同数量的floats来填充一个结构体。由于这个库的目的是通过以太网发送消息,所以我只需要迭代两次-一次发送和一次接收。所有其他时间,这些数据都存储为数组。我知道-我应该将数组序列化并按原样发送,但我重申-我绝对不能这样做。

我有一个float[1024],必须迭代整个数组并填充以下结构体:

struct pseudovector
{
    float data1;
    float data2;
    float data3;
    ...
    float data1024;
}

我已经使用BOOST_PP_REPEATBOOST_PP_SEQ_FOR_EACH_I生成了这个结构体,这样我就不必写出所有1024个浮点数,而且可以提高可维护性/可扩展性。

同样地,我尝试通过预编译器的##连接方式来迭代结构体(https://stackoverflow.com/a/29020943/2066079),但由于这是在预编译器时完成的,因此无法用于运行时的获取/设置。

我已经研究过实现反射,例如如何向C++应用程序添加反射?Ponder库,但这两种方法都需要显式地写出每个可以反射的项。在这种情况下,我可能会创建一个std::map<string, float>并通过字符串/整数连接在for循环中进行迭代:

for(i=0;i<1024;i++)
{
    array[i] = map.get(std::string("data")+(i+1))
}

有没有人能推荐一个更简洁的解决方案,而不需要我写超过1024行的代码?非常感谢你的帮助!

再次重申 - 我绝对不能使用任何形式的数组/向量。


这似乎是反模式。你能否将这个结构体包装在一个联合体中?union wrapper { float arr[1024]; pseudovector vec; }; - kmdreko
3
为什么不直接使用以下代码:float[1024] fa; pseudovector pv; memcpy(fa,&pv, sizeof(pv)); - c-smile
@c-smile 因为这是未定义行为。 - dberm22
4个回答

9
这可能比你预想的要容易得多。首先,需要注意以下几点:
  1. 数组根据标准保证是连续的;也就是说,在它们之间没有插入填充,并且数组本身与元素类型的对齐要求相匹配。

  2. 结构体没有这样的限制;它们可能受到任意填充的影响。然而,在给定版本的某个实现中,它们以所有翻译单元的相同方式进行操作(否则,如何使用相同的结构定义来跨翻译单元传递数据?)。通常情况下,特别是当结构体仅包含一个单一类型的成员时,会以相当理智的方式进行处理。对于这样的结构体,对齐通常与成员的最大对齐相匹配,而且通常没有填充,因为所有成员具有相同的对齐。

在你的情况下,你的1024个浮点数组和你的1024个浮点成员结构体 极有可能 有完全相同的内存布局。这 绝对不是标准所保证的,但是你的编译器可能会记录其结构体布局规则,而且你可以始终在单元测试中断言大小和对齐匹配(你已经有单元测试了,对吧?)

鉴于这些注意事项,你几乎可以肯定地将两者之间简单地进行 reinterpret_cast (或 memcpy)。


3
您可以使用类型转换将结构体视为数组。
float array[1024] = { ... };
pseudovector pv1;
float *f = static_cast<float*>(static_cast<void*>(&pv1));
for (int i = 0; i < 1024; i++) {
    f[i] = array[i];
}

只要您对未定义的行为没有问题。 - Alan Stokes
既然他通过以太网发送结构体,显然他对此没有问题。编写它的代码几乎肯定会做出相同的连续数据假设。 - Barmar
好的,谢谢!这个可以工作是因为结构体中的所有项都是浮点数,对吗?如果它们是混合类型(即每个结构体中有头信息),那么就不能使用这个方法了? - dberm22
static_cast不会执行该转换(无需通过void*进行)。 - Andrew
@Andrew 我承认我对所有的C++强制类型转换运算符都不太熟悉,哪个是正确的呢? - Barmar
reinterpret_cast 是唯一一个可以在不相关类型之间进行转换的。或者使用 static_cast<float*>(static_cast<void*>(&pv1));,该操作通过 void* 进行中间转换。这两种方法都属于技术上的未定义行为,但如果你知道内存布局是相同的,你应该能够避免问题。 - Andrew

2
您可以使用预处理器元编程来创建数组。您可以这样做:
#define ACCESSOR(z, n, type) &type::data ## n

auto arr[] = {
    BOOST_PP_ENUM(1000, ACCESSOR, pseudovector)
};

很可能需要调整 ACCESSOR。在此处使用 auto 可能是不合法的。

然后你可以执行以下操作:

auto val = (pv1.*arr)[4];

不需要使用 UB。


2
如果您能够使用最近版本的Clang或GCC编译,请使用Boost.Hana(据我所知,这是唯一支持的编译器)。
让我引用教程中的Introspection部分:

静态内省,正如我们在这里讨论的那样,是程序在编译时检查对象类型的能力。换句话说,它是一种以编程方式与类型在编译时交互的接口。例如,您是否曾经想过要检查某个未知类型是否具有名为foo的成员?或者在某些时候,您需要遍历结构体的成员吗?

对于introspecting user-defined types,您确实需要使用Hana定义struct,但这与普通定义struct并没有太大区别。
struct pseudovector {
   BOOST_HANA_DEFINE_STRUCT(pseudovector,
    (float, data1),
    (float, data2),
    …
  );
};

你应该很容易地修改任何宏来生成你当前的结构体,以便生成这个新的结构体。
这将在“pseudovector”中添加一个嵌套的结构体,该结构体仅包含静态成员函数。它不会影响 POD、大小或数据布局。
然后你可以像这个例子一样遍历它:
pseudovector pv;

hana::for_each(pv, [](auto pair) {
  std::cout << hana::to<char const*>(hana::first(pair)) << ": "
            << hana::second(pair) << std::endl;
});

在这里,pair 的成员是一个 hana 编译时字符串(成员名称)和其值。如果你更愿意让你的 lambda 函数接受两个参数(名称和值),可以使用 hana::fuse
hana::for_each(pv, hana::fuse([](auto name, auto member) {
  std::cout << hana::to<char const*>(name) << ": " << member << std::endl;
}));

@Andrew:完成了。顺便说一句,当时我考虑的是通过引用文档来展示Hana能够实现所需的功能。 - Nick Matteo

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