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

Using variadic templates for chains of templated classes to generate serialization

本文关键字:参数 序列化 变参      更新时间:2023-10-16

>我有一个热切的项目,在这个项目中,我试图通过编写如下内容来尽可能轻松地启用结构的序列化:

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);
}

编译器抱怨我对字符串名称的调用格式不正确。尽管 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的属性。

由于您实际上无法将参数包扩展为语句,因此您可以使用技巧。我将使用此处的技巧将语句隐藏在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 爆炸(错误 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简化,后来切换到非递归解决方案,希望有更好的编译时间。