Search code examples
c++qttemplatesvariadic-templatestemplate-templates

Using variadic templates for chains of templated classes to generate serialization


I have an eager project in which I try to enable serialization of structs as effortlessly as possible by writing something along the lines of this:

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, ...>;
}

which should output a json object. I recently found out there are variadic templates in c++ and was very excited to see if I could define such a Serializer template that takes an arbitrary amount of Fields and then serializes them. However I was stuck at the following code:

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

This appears to compile but I have not yet been able to get it to work in practice. Namely I am trying to use it like this:

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

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

Compiler complains that my call to stringName is ill-formed. Despite that a test instantiation of Field<...> seems to work, the call to the function does not with error code:

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

I'm scratching my head as to what I'm doing wrong or if this is possible at all.


Solution

  • This is possible, but the right tool isn't template templates. To dig into type parameters, like you want to do by extracting all the template parameters of Field, you need to use partial template specialization.

    Since this can all be simplified a bit in C++17, I'm going to split this into two:

    C++11 Solution

    To start, simplify Field so it's a regular template:

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

    Partial template specialization is not supported for function templates, so the next step is to make a dummy struct. You can actually deduce everything we need from the fields, so the fields are the only necessary type parameters:

    template <typename... Fields>
    struct ObjectSerializer;
    

    Now, it gets fun. Turn each parameter of Field into a parameter pack, and expand them to get the specialized type:

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

    In the body of this monstrosity template, use the call operator to define the actual function. The body of this function should set a property of object to the value extracted to a field.

    Since you can't actually expand a parameter pack into statements, you have you use tricks. I'm going to use the trick from here to hide the statements in an std::initializer_list, in such a way that everything but the assignments are constant-folded:

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

    And then you can wrap the whole thing in a convenience function to hide the struct. I rearranged it a bit from yours so Invokee is deduced from the argument:

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

    After that, tryItOut() will work like you expect:

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

    Demo: https://godbolt.org/z/kHTmPE

    Simplified C++17 Solution

    If C++17 is available to you, you can actually make this a bit nicer by using auto non-type template deduction. For the field, use auto in place of the getter, and get rid of the details:

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

    But when you partially specialize, you can still deduce all that information. You can also use fold expressions to simplify the "expand assignment" trick:

    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)()), ...);
        }
    };
    

    So now, serializeToObject only needs two template arguments per field instead of 4:

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

    Demo: https://godbolt.org/z/UDinyi

    Works find in clang. But ouch, this causes gcc to explode (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,
    

    (I will send a full bug report shortly)

    Simplified C++17 Solution (with gcc workaround)

    That gcc bug sucks, but it can be worked around by using a different type to serialize each field:

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

    That generates more types than you'd probaby like, but not as many types as, say, a recursive solution.

    Demo: https://godbolt.org/z/kMYBAy


    EDITs: I've revised this answer a few times, first to add the C++17 simplification, and later to switch to a non-recursive solution that hopefully has better compile times.