When developing C++ code, I often find myself trying to do something for all the data members belonging to a class. Classic examples are in the copy constructor and assignment operator. Another place is when implementing serialization functions. In all of these situations, I have spent a lot of time tracking down bugs in large codebases where someone has added a data member to a class but has not added its usage to one of these functions where it is needed.
With C++11, 14, 17, and 20, there are many template programming techniques that are quite sophisiticated in what they can do. Unfortunately, I understand only a little of template metaprogramming. I am hoping that someone can point me to a way to specify a list of variables (as a class member and/or type) that I can use to help reduce errors where someone has inadvertently left a member out. I am okay with both a compile-time and run-time penalty, as long as there is an easy way at build time to specify whether or not to use such instrumentation.
A notional usage might look like:
class Widget {
template <typename Archive> void serialize(Archive ar) {
auto myvl = vl(); // make a new list from the one defined in the constructor
ar(a);
ar(x);
myvl.pop(a);
myvl.pop(x);
// a run-time check that would be violated because s is still in myvl.
if (!myvl.empty())
throw std::string{"ill-definied serialize method; an expected variable was not used"};
// even better would be a compile-time check
}
private:
int a;
double x;
std::string s;
VariableList vl(a, x, s);
};
Or perhaps some static analysis, or ...
I am just looking for a way to improve the quality of my code. Thanks for any help.
This is no way to do this without reflection support. The alternative way is to transform your customized struct
into the tuple
of your member reference then using std::apply
to operate the elements of the tuple
one by one. You can see CppCon 2016: "C++14 Reflections Without Macros, Markup nor External Tooling" for the details. Here are the concepts:
First, we need to detect your customized struct
's fields count:
template <auto I>
struct any_type {
template <class T> constexpr operator T& () const noexcept;
template <class T> constexpr operator T&&() const noexcept;
};
template <class T, auto... Is>
constexpr auto detect_fields_count(std::index_sequence<Is...>) noexcept {
if constexpr (requires { T{any_type<Is>{}...}; }) return sizeof...(Is);
else
return detect_fields_count<T>(std::make_index_sequence<sizeof...(Is) - 1>{});
}
template <class T>
constexpr auto fields_count() noexcept {
return detect_fields_count<T>(std::make_index_sequence<sizeof(T)>{});
}
Then we can transform your struct
into tuple
according to the fields_count
traits (to illustrate, I only support the fields_count
up to 8):
template <class S>
constexpr auto to_tuple(S& s) noexcept {
if constexpr (constexpr auto count = fields_count<S>(); count == 8) {
auto& [f0, f1, f2, f3, f4, f5, f6, f7] = s;
return std::tie(f0, f1, f2, f3, f4, f5, f6, f7);
} else if constexpr (count == 7) {
auto& [f0, f1, f2, f3, f4, f5, f6] = s;
return std::tie(f0, f1, f2, f3, f4, f5, f6);
} else if constexpr (count == 6) {
auto& [f0, f1, f2, f3, f4, f5] = s;
return std::tie(f0, f1, f2, f3, f4, f5);
} else if constexpr (count == 5) {
auto& [f0, f1, f2, f3, f4] = s;
return std::tie(f0, f1, f2, f3, f4);
} else if constexpr (count == 4) {
auto& [f0, f1, f2, f3] = s;
return std::tie(f0, f1, f2, f3);
} else if constexpr (count == 3) {
auto& [f0, f1, f2] = s;
return std::tie(f0, f1, f2);
} else if constexpr (count == 2) {
auto& [f0, f1] = s;
return std::tie(f0, f1);
} else if constexpr (count == 1) {
auto& [f0] = s;
return std::tie(f0);
} else if constexpr (count == 0) {
return std::tie();
}
}
Then you can use this utility in your own serialize
functions:
struct Widget {
template <typename Archive>
void serialize(Archive ar) {
std::apply([ar](auto&... x) { (ar(x), ...); }, to_tuple(*this));
}
};
See godbolt for the live demo.