2

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.

  • [OT]: returning const object is useless (and even worse as it forbid some optimisations). – Jarod42 Dec 13 '19 at 21:55
  • You might be interested by `BOOST_HANA_DEFINE_STRUCT` or equivalent. (As you use Qt, [Q_PROPERTY](https://doc.qt.io/qt-5/properties.html) might help). – Jarod42 Dec 13 '19 at 22:18
  • Why don't use ready-made serialization libraries, such as [cereal](http://uscilab.github.io/cereal/index.html) ? – Vladimir Bershov Dec 14 '19 at 20:23
  • @Jarod42 you mean in the constant field naming function? Do you have an article on that? I'd love to read it up. – PandaOnsen14 Dec 16 '19 at 09:37
  • @VladimirBershov Well an additional library is always one more to keep track of. But I can see how someone would use it if that weren't a problem. – PandaOnsen14 Dec 16 '19 at 09:38
  • @PandaOnsen14: [tutorial example of boost hana](https://www.boost.org/doc/libs/1_61_0/libs/hana/doc/html/index.html#tutorial-introspection-json) to build a json. `Q_PROPERTY` also allows to have introspection. – Jarod42 Dec 16 '19 at 09:47

1 Answers1

1

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.

parktomatomi
  • 3,851
  • 1
  • 14
  • 18
  • That's grand. I'd like to buy you a beer good sir, this opens up a lot of other template shenanigans I am sure to try out. I think the compiler might generate a lot of intermediate structs for this though. – PandaOnsen14 Dec 16 '19 at 09:36
  • Right, all recursive solutions involve a typesplosion that has a pretty negative effect on compilation. It would be better if you could just expand the parameter packs or use a fold exprssion, but I didn't think you could expand a partial specialization. But I'll be a monkey's uncle: https://godbolt.org/z/E-vDAQ ... on clang. It made gcc segfault! So woohoo, I get to file a bug report. – parktomatomi Dec 16 '19 at 16:23
  • Ok, I revised the solution to add a non-recursive solution, as well as a workaround for the gcc bug I found. – parktomatomi Dec 16 '19 at 18:15