Struct Iteration Through Abuse of the C Preprocessor
natecraun.net
natecraun.net
I did both of these things for Grim Fandango for vertex/uniform buffers, so that the game could use buffers without caring about whether it was using OpenGL or console APIs - but it's not open source :-(
Even if you don't need something like this under normal circumstances, consider whether it could let you identify mistakes in debug builds, or avoid errors when writing your serialization code.
INTROSPECTION_DEFINE_STRUCT(S,
(int, anInt),
(bool, aBool),
(float, aFloat),
(double, aDouble),
(std::string, aString)
);
Then I create an instance of it: S s{ 23, true, 37.5f, 42.25, "String" };
And finally I can e.g. iterate over all fields and output their value using a generic lambda: forEachField(s, [](auto v) {
std::cout << v;
});
There are also definitions for the field's type, offset, name etc. generated by the macro, so it's easy to create a binding for vertex attributes, map the struct to a table in Lua, serialize/deserialize it and so on.I'm really hoping C++ gets introspection soon. There are some proposals like https://isocpp.org/files/papers/n3996.pdf and a working group at https://groups.google.com/a/isocpp.org/forum/#!forum/reflect... but I'm not holding my breath for 2017.
struct S {
// How many fields are in this struct?
static constexpr unsigned int FIELD_COUNT = 5;
// Full specializations are not allowed inside classes, so add a dummy parameter <T> that is equivalent to S
template <unsigned int Index, typename T = S>
struct FieldTraits;
// Partial specialization for the first field
template <typename T>
struct FieldTraits<0, T> {
using type = int;
static constexpr std::size_t OFFSET = offsetof(S, anInt);
static constexpr const char* name() { return "anInt"; }
static constexpr type& value(S& self) { return self.anInt; }
};
// The first field itself
int anInt;
// Partial specialization for the second field
template <typename T>
struct FieldTraits<1, T> {
using type = bool;
static constexpr std::size_t OFFSET = offsetof(S, aBool);
static constexpr const char* name() { return "aBool"; }
static constexpr type& value(S& self) { return self.aBool; }
};
// The second field itself
bool aBool;
...
};
Now that we have the meta-information about the struct's fields, we can process them: // Meta-function to process a struct's fields
template <typename T, unsigned int FieldIndex, unsigned int FieldCount>
struct ForEach {
template <typename Functor>
static void apply(T& object, Functor f)
{
// Get the field's value and call the functor with it
auto value = T::FieldTraits<FieldIndex>::value(object);
f(value);
// Next field
ForEach<T, FieldIndex + 1, FieldCount>::apply(object, f);
};
};
// Partial specialization to end iteration
template <typename T, unsigned int FieldCount>
struct ForEach<T, FieldCount, FieldCount> {
template <typename Functor>
static void apply(T& object, Functor f)
{
};
};
And finally a bit of syntactic sugar: template <typename T, typename Functor>
void forEachField(T& object, Functor f)
{
ForEach<T, 0, T::FIELD_COUNT>::apply(object, f);
}
*(omitted for the sake of clarity: dealing with const objects, using rvalue references and std::forward for the functor to make the code work with non-copyable functors)In the end I end up defining structs like this: SERIALIZER_STRUCT( structname, int8_t, field1, int16_t, field2, ... );
Initializing them like this: structname instance = {.field1 = 1, .field2=2};
and serializing them like this: SERIALIZER_SERIALIZE( structname, &instance, bigbuff_ptr );
As for just using a packed structure, there are definitely caveats with some compilers (ie: compilers for embedded systems may not handle various edge-cases for misaligned access, particularly when dereferencing pointers to structs.)
Of course, he might go even one step further and specify the structs in some other language, then generate C source files from them, which would also allow you to easily generate consumers for that data format in other languages and now you're reinventing Cap'n Proto.
http://clang.llvm.org/docs/LibASTMatchersReference.html
Now he has the actual compiler doing the work for him.
I hate build systems.
But it still is a cludge. It would be better if C/C++ had a better preprocessor.
#define DEFINE_ENUM(name, variants) \
enum name { \
variants(DEFINE_ENUM_VARIANT_ITSELF) \
}; \
const char *repr_##name(enum name e) { \
switch (e) { \
variants(DEFINE_ENUM_VARIANT_REPR_CASE) \
default: return "?"; \
} \
}
#define DEFINE_ENUM_VARIANT_ITSELF(variant) variant,
#define DEFINE_ENUM_VARIANT_REPR_CASE(variant) \
case variant: return #variant;
// client code:
#define foo_variants(x) \
x(FOO_1) \
x(FOO_2)
DEFINE_ENUM(foo, foo_variants)So an easier approach, but more fragile, is just to define the struct, count up how big you think it should be and then do an
assert(sizeof(struct foo) == FOO_SIZE);
If that assert doesn't pop then you can just do
write(fd, &foo, FOO_SIZE);
I know that is a lot less elegant but it works and isn't doing the translation back and forth.
If you don't like that you might consider using Sun's RPC marshalling code that is (used to be?) part of Linux.