How to design a serializable class such that any non-serialized attribute leads to a compile-time error?

144 views Asked by At

Say you have the following code:

class A {
  bool _attribute1;
};

// Arbitrarily using std::string, not the point of this question
std::string serialize(const A&);

Now a developer adds a new bool _attribute2 to class A and forgets to update the serialize function, which leads to a bug at runtime. (Already been there ?)

Is there a way to turn this issue into compile-time error ? As C++ doesn't support reflection, I have the feeling this is impossible, but I may be missing something.

3

There are 3 answers

7
W.F. On

If you are using c++1z you could make use of structured binding:

struct S {
    bool b;
    //bool c; // causes error
};

int main() {
    S s;
    auto [x] = s;
    (void)x;
}

[live demo]

0
skypjack On

The following one should work with C++11.
A bit tricky indeed, it's based on a comment of @SamVarshavchik:

#include<cstddef>
#include<functional>

template<std::size_t> struct Int { int i; };
template<std::size_t> struct Char { char c; };
template<std::size_t> struct Bool { bool c; };

template<typename, template<std::size_t> class...>
struct Base;

template<template<std::size_t> class... T, std::size_t... I>
struct Base<std::index_sequence<I...>, T...>: T<I>... {};

template<template<std::size_t> class... T>
struct Check final: Base<std::make_index_sequence<sizeof...(T)>, T...> {};

class A final {
    bool _attribute1;
    bool _attribute2;
private:
    char _attribute3;
    // int _attribute4;
};

void serialize(const A &) {
    static_assert(sizeof(A) == sizeof(Check<Bool, Bool, Char>), "!");
    // do whatever you want here...
}

int main() {
    serialize(A{});
}

The basic idea is to list all the types of the data members and define a new type from them with a mixin. Then it's a matter of putting a static_assert in the right place.
Note that private data members are taken in consideration too.

There exist some corner cases that could break it, but maybe it can work for your real code.


As a side note, it can be further simplified if C++14 is an option:

#include<cstddef>

template<typename... T>
constexpr std::size_t size() {
    std::size_t s = 0;
    std::size_t _[] = { s += sizeof(T)... };
    (void)_;
    return s;
}

class A final {
    bool _attribute1;
    bool _attribute2;
private:
    char _attribute3;
    // int _attribute4;
};

void serialize(const A &) {
    static_assert(sizeof(A) == size<bool, bool, char>(), "!");
    // ...
}

int main() {
    serialize(A{});
}
0
W.F. On

If you are doomed to use c++11 and still you are interested in serializing only the public fields you could create trait testing if the type can be constructed using list initialization with a given parameters types but not even one more (of any type):

#include <type_traits>

struct default_param {
    template <class T>
    operator T();
};

template <class T, class...>
using typer = T;

template <class, class, class... Args>
struct cannot_one_more: std::true_type {};

template <class Tested, class... Args>
struct cannot_one_more<typer<void, decltype(Tested{std::declval<Args>()..., default_param{}})>, Tested, Args...>: std::false_type {
};

template <class...>
struct is_list_constructable: std::false_type {};

template <class Tested, class... Args>
struct is_list_constructable<Tested(Args...)>: is_list_constructable<void, Tested, Args...> { };

template <class Tested, class... Args>
struct is_list_constructable<typer<void, decltype(Tested{std::declval<Args>()...}), typename std::enable_if<cannot_one_more<void, Tested, Args...>::value>::type>, Tested, Args...>: std::true_type { };

struct S {
    bool b;
    //bool c; // causes error
};

int main() {
    static_assert(is_list_constructable<S(bool)>::value, "!");
}

[live demo]