0

For example say I have a class Foo with a typical serialization pattern.

struct Foo {
    int a;
    bool b;
    template <typename Archive>
    void serialize(Archive & ar) {
        ar & a;
        ar & b;
    }
};

Now suppose somebody comes along and adds a field.

struct Foo {
    int a;
    bool b;
    std::string c;

    template <typename Archive>
    void serialize(Archive & ar) {
        ar & a;
        ar & b;
    }
};

This compiles just fine but the Archive method is wrong. I could add something like


namespace detail {
template <int a, int b>
void AssertSizeOfImplementation() {
    static_assert(a == b, "Size does not match (check stack trace)");
}
}  
template <typename T, int size>
void AssertSizeOf() {
    detail::AssertSizeOfImplementation<sizeof(T), size>();
}


struct Foo {
    int a;
    bool b;

    template <typename Archive>
    void serialize(Archive& ar) {
        ar& a;
        ar& b;
        AssertSizeOf<Foo,8>();
    }
};

and now if I add the extra field I will see a compile error

: In instantiation of 'void detail::AssertSizeOfImplementation() [with int a = 40; int b = 8]':

I can then fix my serialization code and update the assertion.

However this is not very portable. sizeof will return different results depending on packing and architecture.

Are there any other alternatives to using sizeof?

( play with this on godbolt https://godbolt.org/z/fMo8ETjnr )

bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217
  • I suspect this is a duplicate of this question. https://stackoverflow.com/questions/6844605/c-macro-metaprogram-to-determine-number-of-members-at-compile-time ?? – bradgonesurfing May 19 '22 at 08:11
  • C++ does not natively support reflection, so doesn't provide such a thing. Some frameworks (e.g. CORBA implementations) do support reflection, but depend on capabilities outside realms of C++ (e.g. automatic code generators) to provide a basis for that. In the end, you probably need to fall back on documentation (e.g. which says "if you changes what members this struct has, change the `serialize()` function"), trust in other developers to follow that guidance, and possibly administrative controls (code review, etc) for enforcement. – Peter May 19 '22 at 08:30
  • 1
    So the point is to detect that someone forgot to update the `serialize` function, is that correct? – user253751 May 19 '22 at 08:33
  • @user253751 yes that is the main point. – bradgonesurfing May 19 '22 at 08:40
  • `// If you add or change any data members of this class, update serialize too ` – Paul Sanders May 19 '22 at 08:51
  • @PaulSanders ;) And when they add a GPT15 GAI module to the compiler which can interpret natural language comments and then validate the codebase by figuring out what I mean rather than what I specify all will be good. Till then .... – bradgonesurfing May 19 '22 at 09:29
  • To catch adding or removing members use [static_assert](https://en.cppreference.com/w/cpp/language/static_assert) eg `static_assert(sizeof(Foo) == 42);` (replace `42` with the actual size of `Foo`). However this will not usually catch existing members being rearranged. – Richard Critten May 19 '22 at 09:38
  • @RichardCritten This is exactly what my example does. It's a bit obfuscated but it is the same. The problem is that sizeof does not give consistent results across architectures. It's a halfway good solution that I'm using at the moment. – bradgonesurfing May 19 '22 at 09:42
  • Apart from the reliance on implementation-defined magic numbers - which means your solution adds some maintenance burden - adding (or removing) members to a class is also not guaranteed to change `sizeof` for that class. For example, an added member of a class might fit into space that was used for padding before it is added. – Peter May 19 '22 at 09:57
  • @Peter Yes I'm aware that sizeof trick has limitations. That is the reason for asking for alternatives. In the meantime it is better than nothing. Ideally one day there will be a static reflection capability. https://open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2320r0.pdf – bradgonesurfing May 19 '22 at 10:12
  • But won't the serialize function also fail if any of the data members change size? I would say you should assert the size of both `a` and `b`. And instead of asserting the size of Foo is 8 it would be better to compare it to the number of bytes archived. Something like: `size_t count = 0; count += ar& a; count += ar& b; static_assert(sizeof(Foo) == count);` – Goswin von Brederlow May 19 '22 at 10:17
  • 1
    @Revolver_Ocelot: Comment can easily be ignored (between out-of-date comments, policy with lot of comment, ...). a compiler error or a failing test is harder to ignore. – Jarod42 May 19 '22 at 11:18

1 Answers1

1

There is cool library in boost: boost pfr. I never used it in real project (just some toys), but seems to work quite well:

struct Foo {
    int a;
    bool b;

    template <typename Archive>
    void serialize(Archive& ar, const unsigned int)
    {
        boost::pfr::for_each_field(*this, [&ar](const auto& field) { ar& field; });
    }
};

Live demo.

Now any change in list of fields will be taken into account without need to alter serialization code.

I tried on godbolt, but I've failed to resolve linking issues (can't find boost libraries), so it could actually run.

Marek R
  • 32,568
  • 6
  • 55
  • 140
  • That is super nice. Worst case is you can break a class into an inner aggregate holding the members and an outer abstracting access. It really does solve the problem. – bradgonesurfing May 26 '22 at 19:45