对模板化 类 链使用可变模板来生成序列化

Using variadic templates for chains of templated classes to generate serialization

我有一个急切的项目,在这个项目中,我尝试通过按照以下方式编写一些内容来尽可能轻松地启用结构的序列化:

class Data {
  const QString& string();
  void setString(QString string);
  ...
};

const QString stringName() { return "string"; }

template class <class Invokee, typename ContentType, const QString(*NameFunction)(), const ContentType& (Invokee::* Getter)() const> Field;

void serialize() {
  Data data{...};
  QJsonObject serialized 
    = serialize<Data, Field1, Field2, ...>;
}

应该输出一个 json 对象。我最近发现在 c++ 中有可变参数模板,并且非常兴奋地想看看我是否可以定义这样一个序列化程序模板,它采用任意数量的字段,然后将它们序列化。但是我被困在以下代码中:

template<
    class Invokee,
    typename ContentType,
    const QString(*NameFunction)(),
    const ContentType& (Invokee::* Getter)() const
    >
void serializeToObject(QJsonObject& object, const Invokee& invokee) {
  auto name = NameFunction();

  object[name] = (invokee.*Getter)();
}


template<
      class Invokee,
      template<
          class,
          typename ContentType,
          const QString(*)(),
          const ContentType& (Invokee::* Getter)() const
          > class Field,
      class FieldClass,
      class FieldInvokee,
      typename FieldContentType,
      const QString(*FieldNameFunction)(),
      const FieldContentType& (Invokee::* FieldGetter)() const,

      class... Args
      >
void serializeToObject(QJsonObject& object, const Invokee& invokee) {
  serializeToObject<FieldInvokee, FieldContentType, FieldNameFunction, FieldGetter>(object, invokee);

  serializeToObject<Invokee, Args...>(object, invokee);
}

这似乎可以编译,但我还没有能够让它在实践中工作。也就是说,我正在尝试这样使用它:

void tryOut() {
  Data data;
  data.setString("testString");
  QJsonObject object{};

  serializeToObject
      <
      Data,
      Field<Data, QString, stringName, &Data::string>
      >
  (object, testClass);
}

编译器抱怨我对 stringName 的调用格式错误。尽管 Field<...> 的测试实例似乎有效,但对该函数的调用没有错误代码:

candidate template ignored: couldn't infer template argument 'NameFunction'
void serializeToObject(QJsonObject& object, Invokee& invokee) {

我正在摸不着头脑,想知道我做错了什么,或者这是否可能。

这是可能的,但正确的工具不是模板模板。要深入研究类型参数,就像您想通过提取 Field 的所有模板参数一样,您需要使用部分模板特化。

因为这一切都可以在 C++17 中简化一点,我将把它分成两部分:

C++11 解决方案

首先,简化 Field 使其成为常规模板:

template <
    class Invokee, 
    typename ContentType, 
    const QString(*NameFunction)(), 
    const ContentType& (Invokee::* Getter)() const> 
struct Field;

函数模板不支持部分模板特化,因此下一步是制作一个虚拟结构。你实际上可以从字段中推导出我们需要的一切,所以字段是唯一必要的类型参数:

template <typename... Fields>
struct ObjectSerializer;

现在,它变得有趣了。把Field的每个参数都变成一个参数包,展开得到特化的类型:

template <
    typename Invokee,
    typename... ContentType, 
    const QString(*...NameFunction)(), 
    const ContentType& (Invokee::*...Getter)() const>
struct ObjectSerializer<Field<Invokee, ContentType, NameFunction, Getter>...>
{ /* ... */ }

在这个怪物模板的主体中,使用调用运算符定义实际函数。此函数的主体应将 object 的 属性 设置为提取到字段的值。

由于您实际上无法将参数包扩展为语句,因此您不得不使用技巧。我将使用 here 中的技巧来隐藏 std::initializer_list 中的语句,这样除了赋值之外的所有内容都是常量折叠的:

constexpr void operator ()(QJsonObject& object, const Invokee& invokee) { 
    void(std::initializer_list<nullptr_t> {
        (void(object[NameFunction()] = (invokee.*Getter)()), nullptr)...
    });
}

然后你可以将整个东西包装在一个方便的函数中以隐藏结构。我根据你的重新排列了一下,所以 Invokee 是从参数推导出来的:

template <typename... Fields, typename Invokee>
void serializeToObject(QJsonObject& object, const Invokee& invokee) {
    ObjectSerializer<Fields...>{}(object, invokee);
}

之后,tryItOut() 将按您预期的方式工作:

  serializeToObject<
      Field<Data, QString, stringName, &Data::string>
  >(object, data);

演示:https://godbolt.org/z/kHTmPE

简化的 C++17 解决方案

如果您可以使用 C++17,您实际上可以通过使用自动非类型模板推导使它变得更好一些。对于字段,使用 auto 代替 getter,并删除详细信息:

template <const QString(*NameFunction)(), auto Getter>
class Field;

但是当你部分专门化时,你仍然可以推断出所有这些信息。您还可以使用折叠表达式来简化 "expand assignment" 技巧:

template <
    typename Invokee,
    typename... ContentType, 
    const QString(*...NameFunction)(), 
    const ContentType& (Invokee::*...Getter)() const>
struct ObjectSerializer<Field<NameFunction, Getter>...> {
    template <typename TInvokee = Invokee>
    constexpr void operator ()(QJsonObject& object, const Invokee& invokee) {
        (void(object[NameFunction()] = (invokee.*Getter)()), ...);
    }
};

所以现在,serializeToObject 每个字段只需要两个模板参数,而不是 4 个:

  serializeToObject<
      Field<stringName, &Data::string>
  >(object, data);

演示:https://godbolt.org/z/UDinyi

工程在 clang 中找到。但是哎呀,这会导致 gcc 爆炸 (bug 92969):

during RTL pass: expand
<source>: In function 'void serializeToObject(QJsonObject&, const Invokee&) [with Fields = {Field<stringName, &Data::string>}; Invokee = Data]':
<source>:34:34: internal compiler error: Segmentation fault
   34 |     ObjectSerializer<Fields...>{}(object, invokee);
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
Please submit a full bug report,

(我很快发送完整的错误报告)

简化的 C++17 解决方案(使用 gcc 解决方法)

那个 gcc 错误很糟糕,但可以通过使用不同类型序列化每个字段来解决它:

template <typename Field>
struct FieldSerializer;

template <typename Invokee, typename ContentType, const QString(*NameFunction)(), const ContentType& (Invokee::*Getter)() const> 
struct FieldSerializer<Field<NameFunction, Getter>>{
    void operator()(QJsonObject& object, const Invokee& invokee) {
        object[NameFunction()] = (invokee.*Getter)();
    }  
};

template <typename... Fields, typename Invokee>
void serializeToObject(QJsonObject& object, const Invokee& invokee) {
    (void(FieldSerializer<Fields>{}(object, invokee)), ...);
}

这会生成比您可能喜欢的更多的类型,但不会像递归解决方案那样多。

演示:https://godbolt.org/z/kMYBAy


编辑:我已经修改了这个答案几次,首先是添加 C++17 简化,然后切换到希望具有更好编译时间的非递归解决方案。