I wanted to create a relatively universal way of serialising an object, by doing a memcpy
and generating a unique type ID. Stored together they can be used, for example, by another thread to restore a copy of the object for further processing.
This is used, for example, where one tread has a logging function requiring minimal overhead and converting the object to its logged state is considerably more expensive than making a raw copy.
Some other requirements / design choices:
- IDs should be dense (no gaps)
- IDs should fit the smallest unsigned possible
- No RTTI allowed
- A slight convenience overhead at the runtime restoring side is acceptable (and present in the form of the generated 'if' tree to match an ID to a type)
- Both sides have access to the definition of the mapping
- Handling of constructors with side-effects is left up to the user
Id'd love to hear any critiques or possible pitfalls! Below is the header pasted into a silly example to show the idea and interface:
// This keeps a variable in the final output:
#define KEEP(x) volatile auto x __attribute__((unused))
#include <type_traits>
#include <cstdint>
#include <cstddef>
namespace type_list {
/**
* @brief Extract the N-th type of a set of template arguments
*
* @tparam N Index of type to extract
* @tparam Ts Arguments
*/
template <std::size_t N, typename T, typename... Ts>
struct nth_type {
using type = typename nth_type<N-1, Ts...>::type;
};
template <typename T, typename... Ts>
struct nth_type<0, T, Ts...> {
using type = T;
};
/**
* @brief Extract the N-th type of a set of template arguments
*
* @tparam N Index of type to extract
* @tparam Ts Arguments
*/
template <std::size_t N, typename... Ts>
using nth_type_t = typename nth_type<N, Ts...>::type;
/**
* @brief Find the index of the first matching type `IN` in a set of types.
*
* @tparam IN Type to find
* @tparam T First of type list
* @tparam Ts Rest of type list
*/
template <typename IN, typename T, typename... Ts>
struct index_of_type {
static_assert(sizeof...(Ts) != 0 || std::is_same_v<IN, T>, "No index for type found");
static constexpr const std::size_t value { 1 + index_of_type<IN, Ts...>::value };
};
template <typename IN, typename... Ts>
struct index_of_type<IN, IN, Ts...> {
static constexpr const std::size_t value { 0 };
};
/**
* @brief Find the index of the first matching type `IN` in a set of types.
*
* @tparam IN Type to find
* @tparam Ts Type list
*/
template <typename IN, typename... Ts>
static constexpr const auto index_of_type_v { index_of_type<IN, Ts...>::value };
namespace {
static constexpr void noop(const std::size_t = 0) {}
template <size_t I, typename... Ts>
struct map_visit_impl {
template <typename F, typename E>
static constexpr decltype(auto) visit(const std::size_t id, const void *const ptr, F func, E on_error) {
if (id == I - 1) {
return func(*reinterpret_cast<const nth_type_t<I-1, Ts...> *const>(ptr));
} else {
return map_visit_impl<I - 1, Ts...>::visit(id, ptr, func, on_error);
}
}
template <typename F, typename E>
static constexpr decltype(auto) visit(const std::size_t id, void *const ptr, F func, E on_error) {
if (id == I - 1) {
return func(*reinterpret_cast<nth_type_t<I-1, Ts...> *const>(ptr));
} else {
return map_visit_impl<I - 1, Ts...>::visit(id, ptr, func, on_error);
}
}
};
template <typename... Ts>
struct map_visit_impl<0, Ts...> {
template <typename F, typename E>
static constexpr void visit(const std::size_t id, const void *const, F func, E on_error) {
// If arrived here we have a invalid id
on_error(id);
}
template <typename F, typename E>
static constexpr void visit(const std::size_t id, void *const, F func, E on_error) {
// If arrived here we have a invalid id
on_error(id);
}
};
}
/**
* @brief Create an ID map of a set of types.
*
* @tparam Ts Type list
*/
template <typename... Ts>
struct map {
/**
* @brief Get the type with index `N`
*
* @tparam N Index of type to get
*/
template <std::size_t N>
using type = type_list::nth_type_t<N, Ts...>;
/**
* @brief The ID number (index) of a given type `T`
*
* @tparam T
*/
template <typename T>
static constexpr const std::size_t id { type_list::index_of_type_v<T, Ts...> };
/**
* @brief Number of types stored
*/
static constexpr const std::size_t size { sizeof...(Ts) };
/**
* @brief Convert any given pointer to the type matching `id` and pass
* it to a function `func` as only argument using a `reinterpret_cast`.
*
* @tparam F Function type
* @param id id / index of type
* @param ptr Storage location
* @param func Handler function
* @return Result of handler function
*/
template <typename F, typename E = decltype(noop)>
static constexpr decltype(auto) parse(const std::size_t id, const void *const ptr, F func, E on_error = noop) {
return map_visit_impl<sizeof...(Ts), Ts...>::visit(id, ptr, func, on_error);
}
/**
* @brief Convert any given pointer to the type matching `id` and pass
* it to a function `func` as only argument using a `reinterpret_cast`.
*
* @tparam F Function type
* @param id id / index of type
* @param ptr Storage location
* @param func Handler function
* @return Result of handler function
*/
template <typename F, typename E = decltype(noop)>
static constexpr decltype(auto) parse(const std::size_t id, void *const ptr, F func, E on_error = noop) {
return map_visit_impl<sizeof...(Ts), Ts...>::visit(id, ptr, func, on_error);
}
};
}
// Generate unique types
template <size_t N> struct c {};
// Demo set of types
using map = type_list::map<
uint8_t, uint16_t, uint32_t, int8_t, int16_t, int32_t,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>,
c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>, c<__COUNTER__>
>;
// Just a quick hack to initialise some test data:
char bytes[] = "Test string, bla bla";
uint64_t counter = 0;
void fn(const std::size_t n, const std::size_t i) {
__asm volatile("# LLVM-MCA-BEGIN type_map_overhead");
map::parse(n, &bytes[i], [&](auto& val) {
__asm volatile("# LLVM-MCA-END");
// Needed because the handler needs to apply to any type in the map:
if constexpr (std::is_integral_v<decltype(val)>) {
counter += val;
}
});
}
int main() {
KEEP(k1) = map::id<uint16_t>; // size_t => 1
KEEP(k2) = std::is_same_v<map::type<1>, uint16_t>; // bool => true
fn(0, 0);
fn(1, 1);
fn(2, 2);
KEEP(k4) = counter;
}