C++代码生成

13

在我让C++做它本不应该做的事情的史诗般探索中,我正在尝试组合一个编译时生成的类。

根据预处理器定义(例如大致概念)

CLASS_BEGIN(Name)  
    RECORD(xyz)  
    RECORD(abc)

    RECORD_GROUP(GroupName)  
        RECORD_GROUP_RECORD(foo)  
        RECORD_GROUP_RECORD(bar)  
    END_RECORDGROUP   
END_CLASS

虽然我相当确定我可以使用类似这样的结构(甚至可能使用模板元编程)生成一个从文件系统读取数据的类,但我不知道如何同时生成访问数据的函数和读取数据的函数。

我想最终得到类似这样的一个类:

class Name{
    public:
    xyz_type getxyz();
    void setxyz(xyz_type v);

    //etc

    list<group_type> getGroupName();

    //etc

    void readData(filesystem){
         //read xyz
         //read abc
         //etc
    }
};

有人知道这个是否可能实现吗?

--编辑--

为了澄清此功能的预期用途。我有一些以标准格式存储的文件,想要读取它们。格式已经定义好了,因此不能更改。每个文件可以包含任意数量的记录,每个记录可以包含任意数量的子记录。

众多记录类型都包含不同的子记录集,但它们是被定义好的。例如,高度图记录必须包含高度图,但可以选择包含法线。

因此,我会像这样定义一个记录:

CLASS_BEGIN(Heightmap)  
    RECORD(VHDT, Heightmap, std::string) //Subrecord Name, Readable Name, Type  
    RECORD_OPTIONAL(VNML, Normals, std::string)  
END_CLASS  

为此,我希望输出类似于以下类的功能:

class Heightmap{
    public:
    std::string getHeightmap(){
        return mHeightmap->get<std::string>();
    }
    void setHeightmap(std::string v){
        mHeight->set<std::string>(v);
    }

    bool hasNormal(){
        return mNormal != 0;
    }
    //getter and setter functions for normals go here

    private:
    void read(Record* r){
        mHeightmap = r->getFirst(VHDT);
        mNormal = r->getFirst(VNML);
    }


    SubRecord* mHeightmap, mNormal;
}
我遇到的问题是需要每个预处理器定义两次。一次是为了在类中定义函数定义,另一次是为了创建读函数。由于预处理器纯粹是功能性的,我无法将数据推送到队列并在END_CLASS宏定义上生成类。
我无法想到解决这个问题的方法,但我想知道是否有更深入了解C++的人有解决方法。

我觉得这是可能的。你能更精确地描述你遇到问题的具体位置吗? - i_am_jorf
这样做的好处并不明显,请解释一下。 - anon
6个回答

10
如果您正在寻找一种使用C++代码生成序列化/反序列化数据的方法,我建议您查看Google protobufs(http://code.google.com/p/protobuf/)或Facebook's Thrift (http://incubator.apache.org/thrift/)。 对于protobufs,您可以编写以下数据定义:
message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

随后生成一个名为“Person”的C++类,使您能够加载、保存和访问此数据。还可以生成Python、Java等。


6
您可以尝试使用boost tuples 来解决这个问题。这将导致与您现在所考虑的设计不同,但它应该可以以通用的方式解决问题。
以下示例定义了一个形式为“std::string,bool”的记录,然后从流中读取该数据。
#include "boost/tuple/tuple.hpp"
#include <iostream>
#include <sstream>

using namespace ::boost::tuples;

这些函数用于从输入流中读取数据。第一个重载遇到最后一个记录类型时停止迭代元组:

//
// This is needed to stop when we have no more fields
void read_tuple (std::istream & is, boost::tuples::null_type )
{
}

template <typename TupleType>
void read_tuple (std::istream & is, TupleType & tuple)
{
  is >> tuple.template get_head ();
  read_tuple (is, tuple.template get_tail ());
}

以下类实现Record的getter成员。使用RecordKind作为我们的键,我们可以获取我们感兴趣的特定成员。
template <typename TupleType>
class Record
{
private:
  TupleType m_tuple;

public:
  //
  // For a given member - get the value
  template <unsigned int MBR>
  typename element <MBR, TupleType>::type & getMember ()
  {
    return m_tuple.template get<MBR> ();
  }

  friend std::istream & operator>> (std::istream & is
                                  , Record<TupleType> & record)
  {
    read_tuple (is, record.m_tuple);
  }
};

下一个类型是我们记录的元描述。枚举为我们提供了一个符号名称,我们可以使用它来访问成员,即字段名称。然后元组定义了这些字段的类型:
struct HeightMap
{
  enum RecordKind
  {
    VHDT
    , VNML
  };

  typedef boost::tuple < std::string
                       , bool
                     > TupleType;
};

最后,我们构造一条记录并从流中读取一些数据:
int main ()
{
  Record<HeightMap::TupleType> heightMap;
  std::istringstream iss ( "Hello 1" );

  iss >> heightMap;

  std::string s = heightMap.getMember < HeightMap::VHDT > ();
  std::cout << "Value of s: " << s << std::endl;


  bool b = heightMap.getMember < HeightMap::VNML > ();
  std::cout << "Value of b: " << b << std::endl;
}    

由于这是所有模板代码,因此您应该能够在记录中嵌套记录。


4
这是我在C和C++中经常使用的一种技术,称为“列表宏”。假设您有一个事物列表,如变量、错误消息、解释器操作码或任何需要编写重复代码的内容。在您的情况下,这是类成员变量。
假设这是变量。将它们放入列表宏中,如下所示:
#define MYVARS \
DEFVAR(int, a, 6) \
DEFVAR(double, b, 37.3) \
DEFARR(char, cc, 512) \

声明变量,按照以下方式操作:

#define DEFVAR(typ,nam,inival) typ nam = inival;
#define DEFARR(typ,nam,len) typ nam[len];
  MYVARS
#undef  DEFVAR
#undef  DEFARR

现在,您可以通过重新定义DEFVAR和DEFARR,并实例化MYVARS来生成任何类型的重复代码。

有些人觉得这相当不协调,但我认为这是将预处理器用作代码生成器并实现DRY的完美方法。而且,列表宏本身成为了一个微型DSL。


在C语言中,是的。通常在C++中,我会首先寻找一个TMP解决方案。这可能在一定程度上解释了为什么我在C语言中更加高效。 - Pete Kirkham
我对C和C++的偏好取决于我正在做什么,但我完全理解。 - Mike Dunlavey

2

我可能会尝试使用一个记录 mixin 来实现类似的功能,即在编译时自动地向一个类添加功能

   template<class Base, class XyzRecType>
   class CRecord : public Base
   {
   protected:
      RecType xyz;
   public:
      CRecord() : Base() {}


      RecType Get() {return xyz;}

      void Set(const RecType& anXyz) {xyz = anXyz;}

      void ReadFromStream( std::istream& input)
      {
           ...
      }

   };

   class CMyClass
   {
   };

   int main()
   {
        // now thanks to the magic of inheritance, my class has added methods!
        CRecord<CMyClass, std::string> myClassWithAStringRecord;

        myClassWithAStringRecord.Set("Hello");

   }

1
通常情况下,如果您将所有内容合并到一个宏中,然后利用Booost预处理器库来定义您的类,您可以完全实现您想要的功能。请看我如何实现MACE_REFLECT宏,它对整个类进行了部分特化,并且必须在不同的部分中两次引用每个名称。
这与我如何使用预处理器自动将JSON解析为结构体非常相似。
根据您的示例,我会这样翻译:
struct Name {
   xyz_type xyz;
   abc_type abc;
   boost::optional<foo_type> foo;
   boost::optional<bar_type> bar;
};
MACE_REFLECT( Name, (xyz)(abc)(foo)(bar) )

现在我可以从我的解析器中“访问”Name的成员了:
struct visitor {
  template<typename T, T  p>
  inline void operator()( const char* name )const {
        std::cout << name << " = " << c.*p;
  }
  Name c;
};
mace::reflect::reflector<Name>::visit(visitor());

如果您的对象可以表示为结构体、数组、键值对和基本类型,那么这种技术可以奇妙地工作,并且可以让我立即将其序列化/反序列化为JSON/XML或您自定义的记录格式。

https://github.com/bytemaster/mace/blob/master/libs/rpc/examples/jsonv.cpp


因为它的酷炫而被点赞,但是据我所知(HYGWIMBTA =“希望你能理解我这个首字母缩略词的意思”),他不想两次写字段名。你的concat.宏需要这样做。 - Sz.

0

在某些情况下,我不确定您正在寻找什么。

  • 规范中foo和bar会发生什么?
  • getGroupName实际返回什么?(foo,bar)?还是GroupName?

看起来您正在尝试创建一种加载和访问任意布局的磁盘结构的机制。这准确吗?(编辑:刚注意到“set”成员函数...所以我想您正在寻找完整序列化)

如果您在*nix系统上,在Makefile中指定自己的编译器以编译为.o(可能是perl/python/what-have-you脚本,最后调用gcc),这是一个微不足道的解决方案。其他人可能知道在Windows上执行此操作的方法。


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