【问题标题】:Using variadic templates for chains of templated classes to generate serialization对模板类链使用可变参数模板来生成序列化
【发布时间】:2019-12-13 18:23:38
【问题描述】:

我有一个急切的项目,我尝试通过编写类似以下内容的内容来尽可能轻松地启用结构的序列化:

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) {

我在摸索自己做错了什么,或者这是否可能。

【问题讨论】:

  • [OT]:返回 const 对象是无用的(更糟糕的是,它禁止一些优化)。
  • 您可能对BOOST_HANA_DEFINE_STRUCT 或同等人员感兴趣。 (当您使用 Qt 时,Q_PROPERTY 可能会有所帮助)。
  • 为什么不用现成的序列化库,比如cereal
  • @Jarod42 你的意思是在常量字段命名函数中?你有这方面的文章吗?我很想读一读。
  • @VladimirBershov 好吧,另外一个库总是需要跟踪的。但如果这不是问题,我可以看到有人会如何使用它。

标签: c++ qt templates variadic-templates template-templates


【解决方案1】:

这是可能的,但正确的工具不是模板模板。要深入挖掘类型参数,就像你想通过提取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;

但是当你部分专业化时,你仍然可以推断出所有这些信息。您还可以使用折叠表达式来简化“扩展赋值”技巧:

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

作品在铿锵声中找到。但是哎呀,这会导致 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 简化,然后切换到希望有更好编译时间的非递归解决方案。

【讨论】:

  • 太棒了。先生,我想给你买杯啤酒,这开启了很多其他的模板恶作剧,我一定会尝试。我认为编译器可能会为此生成很多中间结构。
  • 是的,所有递归解决方案都涉及对编译有相当负面影响的 typesplosion。如果您可以仅扩展参数包或使用折叠表达式会更好,但我认为您不能扩展部分专业化。但我会成为猴子的叔叔:godbolt.org/z/E-vDAQ ... on clang。它使 gcc 段错误!所以哇哦,我要提交一个错误报告。
  • 好的,我修改了解决方案,添加了一个非递归的解决方案,以及我发现的 gcc 错误的解决方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-10-01
  • 1970-01-01
  • 2017-10-15
  • 1970-01-01
  • 2016-01-22
  • 2015-06-08
  • 2014-10-15
相关资源
最近更新 更多