【问题标题】:How to design a class with "annotated" fields?如何设计带有“注释”字段的类?
【发布时间】:2012-03-27 20:20:44
【问题描述】:

假设我们有某种协议,其中包含数百种消息类型,我们希望通过 C++ 类对每种类型进行建模。由于每个类都应该能够自动处理每个字段,一个自然的解决方案是只拥有一个包含所有必需类型的 std::tuple

std::tuple<int, double, char> message;

print(message);   // the usual variadic magic

这一切都很好。但是,现在我想给每个字段一个名称,并且我希望能够在我的代码中引用该字段时使用该名称,并获得它的文本表示。天真地,或者在 C 中,我可能会写:

struct Message
{
    int    header;
    double temperature;
    char   flag;
};

这样我们就失去了元组的递归自动处理能力,但我们可以按字面意思命名每个字段。在 C++ 中,我们可以通过枚举来做到这两点:

struct Message
{
    enum FieldID { header, temperature, flag };
    static const char * FieldNames[] = { "header", "temperature", "flag" };

    typedef std::tuple<int, double, char> tuple_type;

    template <FieldID I>
    typename std::tuple_element<I, tuple_type>::type & get()
    { return std::get<I>(data); }

    template <FieldID I>
    static const char * name() { return FieldNames[I]; }

    tuple_type data;
};

现在我可以说,Message m; m.get&lt;Message::header&gt;() = 12; 等,我可以对字段进行递归,并让每个字段打印出以自己的名称为前缀的自己的值,等等。


现在的问题是:我怎样才能有效地编写这样的代码而不重复?

理想情况下,我希望能够这样说:

START_MESSAGE(Message)
ADDFIELD(int, header)
ADDFIELD(double, temperature)
ADDFIELD(char, flag)
END_MESSAGE

有没有什么方法可以结合预处理器、Boost 和 C++11 来实现这样的目标,而不需要外部生成工具? (我认为 Boost.Preprocessor 称之为“水平”和“垂直”重复。我需要以某种方式“转置”字段数据。)这里的关键特征是我永远不必重复任何信息,并且修改或添加一个字段只需要一次更改。

【问题讨论】:

  • 对于此类问题,从长远来看,最好使用简单的描述性语言和生成包含文件的自定义预处理器。 “源文件”易于维护,甚至可以在项目需要时由外部工具生成。
  • @AlexandreC.:在任何给定阶段,“再多一个小工具”总是看起来更好的答案。但在宏伟的计划中,你只需要携带一件东西来维护、记录、记住和培训人们。开箱即用的东西绝对值得设置一些可怕的宏的痛苦。

标签: c++ class-design field


【解决方案1】:

您可以使用 boost 的预处理器序列来做到这一点。

#define CREATE_MESSAGE(NAME, SEQ) ...

CREATE_MESSAGE(SomeMessage,
  (int)(header)
  (double)(temperature)
  (char)(flag)
)

您需要遍历每一对来生成定义。我手头没有任何示例代码,不过如果有趣的话我可能会安排一些。

有一次,我有一个类似这样的生成器,它还生成了字段的所有序列化。我有点觉得它走得太远了。我觉得该领域的具体定义和声明性访问者更直接。万一其他人不得不在我之后维护代码,这就不那么神奇了。我不知道你的情况,只是在实施之后我仍然有保留。 :)

用 C++11 的特性再看一遍会很酷,虽然我没有机会。

更新:

仍有一些问题需要解决,但这主要是可行的。

#include <boost/preprocessor.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/arithmetic/mod.hpp>
#include <boost/preprocessor/control/if.hpp>

#include <tuple>

#define PRIV_CR_FIELDS(r, data, i, elem) \
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),elem BOOST_PP_COMMA,BOOST_PP_EMPTY)()

#define PRIV_CR_STRINGS(r, data, i, elem) \
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),BOOST_PP_STRINGIZE(elem) BOOST_PP_COMMA,BOOST_P

#define PRIV_CR_TYPES(r, data, i, elem) \
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),BOOST_PP_EMPTY,elem BOOST_PP_COMMA)()

#define CREATE_MESSAGE(NAME, SEQ) \
    struct NAME { \
        enum FieldID { \
            BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_FIELDS, _, SEQ) \
        }; \
        std::tuple< \
            BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_TYPES, _, SEQ) \
        > data;\
        template <FieldID I> \
            auto get() -> decltype(std::get<I>(data)) { \
                return std::get<I>(data); \
            } \
        template <FieldID I> \
            static const char * name() { \
                static constexpr char *FieldNames[] = { \
                    BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_STRINGS, _, SEQ) \
                }; \
                return FieldNames[I]; \
            } \
    };

CREATE_MESSAGE(foo,
        (int)(a)
        (float)(b)
    )

#undef CREATE_MESSAGE

int main(int argc, char ** argv) {

    foo f;
    f.get<foo::a>() = 12;

    return 0;
}

get 的 decltype 有问题。我还没有真正使用元组来知道那里会发生什么。不过,我认为这与您生成类型或字段的方式没有任何关系。

这是预处理器使用 -E 生成的内容:

struct foo { 
  enum FieldID { a , b , }; 
  std::tuple< int , float , > data;
  template <FieldID I> 
    auto get() -> decltype(std::get<I>(data)) { 
      return std::get<I>(data); 
  } 
  template <FieldID I> static const char * name() { 
    static constexpr char *FieldNames[] = { "a" , "b" , }; 
    return FieldNames[I]; 
  } 
};

【讨论】:

  • 这听起来很有希望。让我阅读它的文档。如果您有更完整的代码示例,我将不胜感激(并且可能会发送一些赏金)。
  • @KerrekSB 这是对试图制作更强类型枚举的人的链接(boost vault)的引用,这应该表明您至少会在序列上进行迭代。 stackoverflow.com/a/439004/839436
  • @KerrekSB 已更新。到目前为止它还没有完全工作,但似乎正在生成结构字段。
  • 非常感谢!我得到了这个想法——我会发布我的解决方案,但我会接受你的回答作为关键的“火花”。
  • @KerrekSB 很高兴听到,我真的只是在想如何定义类型等。我无法让 get 的返回类型正常工作。
【解决方案2】:

这不是一个答案,而只是另一个需要考虑的(可怕的)想法。我有一个inl 文件,我曾经写过它有点相似。在这里:http://ideone.com/6CvgR

基本概念是调用者这样做:

#define BITNAME color
#define BITTYPES SEPERATOR(Red) SEPERATOR(Green) SEPERATOR(Blue)
#define BITTYPE unsigned char
#include "BitField.inl"

inl 文件通过重新定义SEPERATOR 然后再次使用BITTYPES 来创建具有命名成员的自定义位域类型。然后可以轻松使用它,包括ToString 函数。

 colorBitfield Pixel;
 Pixel.BitField = 0; // sets all values to zero;
 Pixel.Green = 1; // activates green;
 std::cout << "Pixel.Bitfield=" << (int)Pixel.BitField << std::endl;  //this is machine dependant, probably 2 (010).
 Pixel.BitField |= (colorBitfield::GreenFlag | colorBitfield::BlueFlag); // enables Green and Blue
 std::cout << "BlueFlag=" << (Pixel.BitField & colorBitfield::BlueFlag) << std::endl; // 1, true.
 std::cout << "sizeof(colorBitField)=" << sizeof(colorBitfield) << std::endl;

内联文件本身就是可怕的代码,但是像这样模糊的一些方法可能会简化调用者的使用。

如果我以后有时间,我会看看我是否可以按照这个想法为你想要的东西做点什么。

【讨论】:

    【解决方案3】:

    根据 Tom Kerr 的建议,我查找了 Boost.Preprocessor 序列。这是我想出的:

    #include <boost/preprocessor/seq.hpp>
    #include <boost/preprocessor/comma_if.hpp>
    #include <boost/preprocessor/arithmetic.hpp>
    #include <boost/preprocessor/stringize.hpp>
    
    #include <tuple>
    
    #define PROJECT1(a,b) a
    #define PROJECT2(a,b) b
    
    #define BOOST_TT_projectqu(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) BOOST_PP_STRINGIZE(PROJECT2 t)
    #define BOOST_TT_project1(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) PROJECT1 t
    #define BOOST_TT_project2(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) PROJECT2 t
    
    
    template <typename T> struct Field { };
    
    #define MESSAGE(classname, data) struct classname                                                \
      {                                                                                              \
          typedef std::tuple<BOOST_PP_SEQ_FOR_EACH(BOOST_TT_project1, ~, data)> tuple_type;          \
                                                                                                     \
          static constexpr char const * FieldNames[BOOST_PP_SEQ_SIZE(data)] = { BOOST_PP_SEQ_FOR_EACH(BOOST_TT_projectqu, ~, data) }; \
                                                                                                     \
          enum FieldID { BOOST_PP_SEQ_FOR_EACH(BOOST_TT_project2, ~, data) };                        \
                                                                                                     \
          template <FieldID I> using type = typename std::tuple_element<I, tuple_type>::type;        \
                                                                                                     \
          template <FieldID I> typename std::tuple_element<I, tuple_type>::type & get() { return std::get<I>(dat); } \
          template <FieldID I> typename std::tuple_element<I, tuple_type>::type const & get() const { return std::get<I>(dat); } \
                                                                                                     \
      private:                                                                                       \
          tuple_type dat;                                                                            \
      };
    
    MESSAGE(message,            \
        ((int, header))         \
        ((double,temperature))  \
        ((char, flag))          \
    )
    

    gcc -std=c++11 -E -P 编译整个东西(并重新格式化)给出:

    template <typename T> struct Field { };
    
    struct message {
        typedef std::tuple< int , double , char > tuple_type;
        static constexpr char const * FieldNames[3] = { "header" , "temperature" , "flag" };
        enum FieldID { header , temperature , flag };
        template <FieldID I> using type = typename std::tuple_element<I, tuple_type>::type;
        template <FieldID I> typename std::tuple_element<I, tuple_type>::type & get() { return std::get<I>(dat); }
        template <FieldID I> typename std::tuple_element<I, tuple_type>::type const & get() const { return std::get<I>(dat); }
        private: tuple_type dat; };
    

    【讨论】:

      【解决方案4】:

      您可以执行类似于 BOOST_SERIALIZATION_NVP(来自 Boost.Serialization 库)所做的事情。该宏创建了一个(短暂的)包装结构,将其参数的名称和值绑定在一起。然后由库代码处理这个名称-值对(名称实际上只在 XML 序列化中很重要,否则将被丢弃)。

      因此,您的代码可能如下所示:

      int    header      = 42;
      double temperature = 36.6;
      char   flag        = '+';
      print (Message () + MY_NVP (header) + MY_NVP (temperature) + MY_NVP (flag));
      

      【讨论】:

      • 嗯,有趣...我想知道 Boost.serialization 是否有任何工具可以帮助完成这项工作。但我真的很想要实际的Message 类。我已经有它的序列化代码,所以我可能只想实例化一个,填充它并将其发送到我的序列化程序,或者将其内容打印到日志文件。
      • @KerrekSB:我想我当时误解了你。您是否想保留Message 类现在的样子并且也可以轻松创建类似的类(Message1,...Message53)?
      • 是的,确实如此。我想(永久地)设计许多消息类,并且我想要一种简单的方法来创作所有这些类。我可以像在示例中那样手动写出每一个,但这将是乏味和可怕的。我也可以编写一个外部工具来创建类定义,但这也很不令人满意。
      猜你喜欢
      • 2019-09-16
      • 1970-01-01
      • 2016-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-02-28
      • 1970-01-01
      • 2016-01-26
      相关资源
      最近更新 更多