c++ - 对模板类链使用可变参数模板来生成序列化

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

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

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代替 setter/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

作品在 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 简化,然后切换到希望有更好编译时间的非递归解决方案。

关于c++ - 对模板类链使用可变参数模板来生成序列化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59327963/

相关文章:

c++ - 从 EEPROM 读取并打印到串口

c++ - QProcess -> 在 Linux 上从 sftp 读取标准输出

qt - 如何停止线程 - Qthread

C++ 模板参数引用该模板中定义的类型

php - 在 CodeIgniter 中的 View 中包含 View 的最佳方法

c++ - 在 gdb 中打印有关文件流对象的详细信息 - C++

c++ - 在 boost::python 中公开 boost::scoped_ptr

c++ - 构建 32 位库 opencv

qt - 使qt的独立可执行文件

javascript - asp.net 中继器在 javascript 中使用模板