c++ - 在多元素框中,迭代结构;轻松显示结构字段和值

  显示原文与译文双语对照的内容

是否有一个简单的方法来显示 struct 字段及其在 RichEdit 控件中的对应值?

这就是我现在要做的:


AnsiString s;

s = IntToStr(wfc.fontColor);
RichEdit1->Lines->Append(s);

等等 。

是否有比单独调用每个人更简单的方法? 我想读取一个二进制文件,然后在 RichEdit 控件中为我正在构建的小工具显示相应的结构,并且没有其他方法。 我知道如何读取二进制文件并将值读入 struct

时间:

BOOST_FUSION_ADAPT_STRUCT 似乎在这里很适合。 例如:


//Your existing struct
struct Foo
{
 int i;
 bool j;
 char k[100];
};

//Generate an adapter allowing to view"Foo" as a Boost.Fusion sequence
BOOST_FUSION_ADAPT_STRUCT(
 Foo,
 (int, i)
 (bool, j)
 (char, k[100])
)

//The action we will call on each member of Foo
struct AppendToTextBox
{
 AppendToTextBox(RichEditControl& Ctrl) : m_Ctrl(Ctrl){}

 template<typename T>
 void operator()(T& t)const
 {

 m_Ctrl.Lines.Append(boost::lexical_cast<std::string>(t));
 }

 RichEditControl& m_Ctrl;

};

//Usage:
void FillTextBox(Foo& F, RichEditControl& Ctrl)
{
 boost::fusion::for_each(F, AppendToTextBox(Ctrl));
}

如果我正确理解,原始问题的核心是如何迭代一个结构。 简而言之,就像Jerry在评论中指出的那样,这不能做。 我会试着解释为什么,然后我会试着解释如何做下一个最好的事情。

一个结构作为一个单独的数据存储在内存中,没有任何描述它的结构的元数据。 例如以下结构:


struct Foo {
 char a;
 char b;
 char c;
 int i;
}

Foo f = {'x', 'y', 'z', 122};

可以用十六进制表示法在内存中表示,如下所示


78 79 7A FF 7A 00 00 00

第一个 3字节包含字符字段,第四是一个随机值用于填充,和接下来的四个字节的little-endian表示整数 122. 这里布局将从编译器和编译器到系统不同。 简而言之,二进制表示不告诉你数据是什么,或者各个字段的存储位置。

那么编译器如何访问结构中的字段? 代码


char c = f.c;

被翻译成一条指令


COPY BYTE FROM ([address of f] + 2) TO [address of c]

换句话说,编译器将字段的文本偏移量编码到代码中。 同样,这并不能帮助我们。

因此,我们必须自己注释结构。 这可以通过在结构中添加信息将它的转换成 key-value 存储或者添加第二个结构来实现。 你不想更改原始结构,因此第二个结构是 go的方式。

我假设你的结构只包含基本类型: int,char,等等 如果你是结构中复杂的其他类,那么我建议将 ToString() 方法添加到它们的基类并调用该方法- 这就是 C# 和 Java 。


Foo tmp;

#define FIELD_OFFSET(f) ((char*)&(tmp.f) - (char*)&tmp)

enum FieldType { INT_FIELD, CHAR_FIELD, OBJECT_FIELD };

struct StructMeta {
 FieldType type;
 size_t offset;
};

StructMeta[] metadata = {
 {CHAR_FIELD, FIELD_OFFSET(a)},
 {CHAR_FIELD, FIELD_OFFSET(b)}, 
 {CHAR_FIELD, FIELD_OFFSET(c)},
 {INT_FIELD, FIELD_OFFSET(i)},
 {OBJECT_FIELD, FIELD_OFFSET(o)},
}

void RenderStruct(Foo* f)
{
 for (int i = 0; i <sizeof(metadata)/sizeof(StructMeta); i++)
 {
 switch (metadata[i].type)
 {
 case CHAR_FIELD:
 char c = *((char*)f + metadata[i].offset);
//render c
 break;
 case INT_FIELD:
 int i = *(int*)((char*)f + metadata[i].offset);
//render i
 break;
 case OBJECT_FIELD:
 Object* o = (object*)((char*)f + metadata[i].offset);
 const char* s = o->ToString();
//render s
 break; 
 }
 }
}

注意:所有指针算法都应该在( char* ) 指针上执行,以确保偏移量被解释为字节。

除非你构建自己的元数据来描述结构,否则无法迭代结构的成员。 C++ 编译器只是不发出你需要的信息。

但是,通过一些宏魔术,你可以很容易地构建你需要的元数据。 几年前我写了一些代码来做这个( 实际上是一个完全吹出的Windows 自定义控件),我一直在使用它。

基本技巧是使用一些宏宏,让编译器帮助你构建元数据。


//this is the structure I want to iterate
typedef struct {
 int foo;
 char bar[16];
} StructIWantToIterate;

//this is the metadata I need for each field of the structure
typedef struct {
 char * pszFieldName;
 size_t oFieldOffset;
 size_t cbFieldSize;
 int eType;
} MyStructMeta;

//these are the field types I need to handle.
enum {
 type_is_int,
 type_is_char,
};

//these macros help to emit the metadata
#define NUMELMS(ary) (sizeof(ary)/(sizeof(ary)[0]))
#define FIELDOFF(tag,fld) ((size_t)&(((tag *)0)->fld))
#define FIELDSIZ(tag,fld) sizeof(((tag *)0)->fld)
#define STDFLD(tag,fld,as) #fld, FIELDOFF(tag,fld), FIELDSIZ(tag,fld), as

//now we declare the metadata for the StructIWantToIterate structure
#undef MYFLD
#define MYFLD(fld,as) STDFLD(StructIWantToIterate,fld,as)
static const MyStructMeta aMeta[] = {
 MYFLD(foo, type_is_int),//expands to"foo", 0, sizeof(int), type_is_int
 MYFLD(bar, type_is_char),//expands to"bar", sizeof(int), 16, type_is_char
};

//and when we want to do the iteration, assume ptr is a pointer to an instance
//of StructIWantToIterate

for (int ii = 0; ii <NUMELMS(aMeta); ++ii)
{
 char szLine[100];//pick your own worst case line size.

//get a pointer to the current field within the struct
 void * pfld = ((byte*)ptr) + aMeta[ii].oFieldOffset;

//print out the field data based on the type_is_xxx information
 switch (aMeta[ii].eType)
 {
 case type_is_int:
 sprintf(szLine,"%s : %d", aMeta[ii].pszFieldName, *(int*)pfld);
 break;

 case type_is_char:
 sprintf(szLine,"%s : %*s", 
 aMeta[ii].pszFieldName, 
 aMeta[ii].cbFieldSize, 
 pfld);
 break;
 }
//send it to the richedit control
 RichEdit1->Lines->Append(asLine); 
}

无法迭代纯结构的成员。 你必须在结构声明之外提供这里信息。

你可以在编译时完成,因为一些以前的答案已经显示出来了。 但是,你也可以在run-time上做。 这类似于一些"序列化"库工作的方式。

你可能有以下类别:


class MemberStore
{
public:
 template<typename Base>
 MemberStore(const Base &base) : 
 m_basePtr(reinterpret_cast<const char*>(&base))
 {}

 template<typename Member>
 MemberStore& operator&(const Member &classMember){
 DataInfo curMember;
 curMember.m_offset = reinterpret_cast<const char*>(&classMember) - m_basePtr;
 curMember.m_func = &CvtChar<Member>;
 m_members.push_back(curMember);
 return *this;
 }

 std::string convert(size_t index) {
 return m_members[index].m_func(m_basePtr + m_members[index].m_offset);
 }

 size_t size() const {
 return m_members.size();
 }

protected:
 template<typename Type> 
 static std::string CvtChar(const void *data) {
 std::stringstream str;
 str <<*reinterpret_cast<const Type*>(data);
 return str.str();
 }

private:
 struct DataInfo {
 size_t m_offset;
 std::string (*m_func)(const void *data);
 };
 std::vector<DataInfo> m_members;
 const char *m_basePtr;
};

这个类别包含一个"类成员"( memberstore:: dataInfo )的向量,每个都有:

  • 从类别基础偏移。
  • 将它们转换为 std::strings的方法。 如果可以使用 std::stringstream 进行转换,则自动生成这里方法。 如果不可能,应该可以使用 specializate 。

你可以使用&运算符( 你可以连接几个&运算符) 向该类添加元素。 然后,你可以对成员进行迭代,并使用它的索引将它们转换为 std::string:


struct StructureIWantToPrint
{
 char a;
 int b;
 double c;
};

int main(int argc, wchar_t* argv[])
{
 StructureIWantToPrint myData;
 myData.a = 'b';
 myData.b = 18;
 myData.c = 3.9;

 MemberStore myDataMembers(myData);
 myDataMembers & myData.a & myData.b & myData.c;

 for(size_t i=0;i<myDataMembers.size();++i) {
 std::cout <<myDataMembers.convert(i) <<std::endl;
 }

 return 0;
}

应该可以修改MemberStore类,这样就不用存储一个方法来将成员转换为 std:: 字符串,而是自动将数据插入到 TextList 。

我不使用 C++ Builder,所以一些细节可能会有些偏离,但是一般的想法应该至少是合理的:


class richedit_stream { 
 TRichEditControl &ctrl;
public:
 richedit_stream(TRichEditControl &trc) : ctrl(trc) {}

 template <class T>
 richedit_stream &operator<<(T const &value) {
 std::stringstream buffer;
 buffer <<value;
 ctrl.Lines->Append(value.str().c_str());
 return *this;
 }
};

基本的想法非常简单: 用于多元素控件的front-end,它提供模板化运算符 <<。 运算符将项目放入stringstream以将它的转换为字符串。 然后获取结果字符串并将它的附加到控件中的行。 因为它是模板化的,它可以使用stringstream支持的所有常用类型。

这确实有缺点--没有更多的工作,你将不能使用操纵器控制数据的格式,因为它被转换为字符串。 因为它使用stringstream来将事情转换为字符串,所以它可能比你的代码显式编码每个转换的类型稍微慢一点。 同时,你可以使用相当简洁,简单,惯用的代码来换取相当少的投资。

建议创建用于写入文本框的模板化方法:


template <typename T>
void
Write_To_Textbox(const T& variable,
 const std::string& variable_name,
 TRichTextEdit & textbox)
{
//...
}

然后使用一些剪切,复制,粘贴和正则表达式表达式替换编辑器函数,并创建一个"批注"函数:


void
annotate(TRichTextEdit& textbox)
{
 Write_To_Textbox(member1,"member1", textbox);
//...
}

注意:检查模板函数的语法,因为我没有在这个例子中找到它。

因为在结构中有一个相当大的字段,使用解析器或者编写自己的源代码来打印成员,它们的名称和它们的值。

作为一个有趣的练习,在编写工具时自己计时。 可以发现使用具有正则表达式搜索和替换功能的编辑器可能会更快。

否则扔掉你当前的设计并采用一个 。 我一直在使用记录和字段设计。 每个记录( 结构) 都有一个指向 Field_Interface的指针的向量。 Field_Interface 有诸如 get_field_name()get_sql_data_type_text() 之类的方法。 也不要忘记将字段值作为字符串返回的Java最爱的toString() 。 这个技术允许你遍历一个字段的容器,并打印出它们的值( 使用 toString ) 和它们的名称( 使用 get_field_name() ) 。

添加访问者 Pattern 用于读取和写入( 我打电话给读者和作者),并且你具有高度适应性的字段和记录,而不更改它的内部内容。 此外,这将导致在不知道它的类型的情况下,在的通用编程中,你可以在不知道它的类型的情况下操作和记录。

顺便说一句,在你等待一个完美的答案,你可以写一个函数"迭代"或者访问的成员结构。

因此,你需要在运行时使类型信息可用。 这里元数据在编译时可用,但随后被丢弃。 我们只需要一种方法来从编译器中解救它。

  1. 显式元数据,如antonmarkov和 John Knoeller所演示。 你必须保持它与结构的同步,但它的优点是不接触你原来的结构定义。

    1.1 代码生成如果你的结构体定义足够普通,你可以使用awk自动化元数据表的生成。

  2. 元编程: 如果你不介意重写结构( 但是保持布局不变,所以保持二进制兼容性),你可以让编译器为你做繁重的提升。 你可以使用 Boost.tuple 来声明你的结构,并使用 Boost.Fusion 语句遍历它的元素。

我认为这不是一个好的答案,但它应该包含在完整的理由中。 一种不同的方法是使用 Windows 调试器扩展 api 。编写调试器扩展。 你所描述的任务几乎非常适合调试器扩展。 我说的几乎是因为我不确定在发布版本中包含它是一个非常好的计划。 但是根据你在哪里需要这个功能,可能会有可能。 如果需要"In-House"作为你自己的目的,它可能会工作。 如果需要在一个客户的站点上运行,那么我就会更倾向于使用它,因为它需要的额外的行李( 调试符号) 。

你的环境也有一个巨大的潜在问题。 似乎你正在使用 C++ Builder 版本 5. 我不知道如何从那个环境生成调试符号,它可以使用 Windows 调试工具。 有一个实用工具 map2dbg 执行转换,但它显然需要 C++ Builder 。

...