I have programmed some serializing functions able to calculate the size, write into a memory buffer and read from a buffer of any struct/class that compliments with these features: containing a specific member type and a 'to_tuple' function.
The methodology is based in returning a std::tie
with all the references to member variables, and assuring that the first one is an enum class variable indicating the specific type. Then std::apply
is used for each function.
For reading from buffer, the read function must be able to process the first member (struct type variable) in a different way, so I've used std::index_sequence idiom to calculate the position of each variable in the tuple, but code looks clumpsy and bloated to me.
I must constraint to c++17, and please, don't mention about how bad idea is to serialize using std::memcpy
due to endianess and these things.
Is there any easier way to detect the first member of a tuple and process it differently while using std::apply
?
Coliru link: https://coliru.stacked-crooked.com/a/1c247e21bfef706b
#include <tuple>
#include <string>
#include <vector>
#include <iostream>
#include <ostream>
#include <cstring>
#include <utility>
enum class TYPES { TYPE_A, TYPE_B, TYPE_C, TYPE_D };
struct Structure
{
int a1;
int a2;
int a3;
bool b1;
double c1;
double c2;
double c3;
double c4;
std::string d1;
std::string d2;
constexpr static inline auto type{ TYPES::TYPE_A };
constexpr auto as_tuple() { return std::tie(type, a1, a2, a3, b1, c1, c2, c3, c4, d1, d2); }
};
// SIZE
template<class T>
constexpr size_t size_of(T&& val) {
using Type = std::decay_t<T>;
if constexpr (!std::is_same_v<Type, std::string>) return sizeof(T);
else return sizeof(char) * val.size() + sizeof(char);
}
template<class... Ts>
constexpr size_t size(std::tuple<Ts...>&& tuple) {
return std::apply([](auto&&... args) { return (size_of(args) + ...); }, tuple);
}
template<class T>
constexpr size_t size(T&& object) {
return size(std::forward<T>(object).as_tuple());
}
// WRITE
template<class T>
constexpr void write_data(unsigned char* &data, T&& val) {
using Type = std::decay_t<T>;
if constexpr (!std::is_same_v<Type, std::string>) { std::memcpy(data, &val, sizeof(T)); data += sizeof(T); }
else { std::memcpy(data, val.c_str(), sizeof(char) * val.size()); data += sizeof(char) * val.size(); *data++ = 0; }
}
template<class... Ts>
constexpr void write(unsigned char* data, std::tuple<Ts...>&& tuple) {
std::apply([&](auto&&... args) { (write_data(data, args), ...); }, tuple);
}
template<class T>
constexpr void write(unsigned char* data, T&& object) {
write(data, std::forward<T>(object).as_tuple());
}
// READ -> std::index_sequence used, is there any other way to achieve this?
template<std::size_t I, class T>
constexpr void read_data(const unsigned char* &data, T&& val) {
using Type = std::decay_t<T>;
if constexpr (I == 0) { static_assert(std::is_same_v<Type, TYPES>); data += sizeof(Type); }
else if constexpr (!std::is_same_v<Type, std::string>) { std::memcpy(&val, data, sizeof(Type)); data += sizeof(Type); }
else { val = std::string{ reinterpret_cast<const char*>(data) }; data += sizeof(char) * val.size() + sizeof(char); }
}
template<std::size_t... I, class... Ts>
constexpr void read(const unsigned char* data, std::tuple<Ts...>&& tuple, std::index_sequence<I...>) {
static_assert(sizeof...(I) == sizeof...(Ts));
std::apply([&](auto&&... args) { (read_data<I>(data, args), ...); }, tuple);
}
template<class T>
constexpr void read(const unsigned char* data, T&& object) {
constexpr auto size = std::tuple_size_v<decltype(std::forward<T>(object).as_tuple())>;
read(data, std::forward<T>(object).as_tuple(), std::make_index_sequence<size>{});
}
template<class... Ts>
void print_tuple(std::tuple<Ts...>&& tuple)
{
std::apply([&](auto&&... args) { bool once{}; ((std::cout << (std::exchange(once, true) ? ", " : "") << args), ...) << "\n"; }, tuple);
}
template<class T>
void print_tuple(T&& object)
{
print_tuple(std::forward<T>(object).as_tuple());
}
std::ostream& operator<< (const std::ostream& out, TYPES type)
{
return std::cout << "Type_" << static_cast<int>(type);
}
int main()
{
Structure original{ 1, 2, 3, true, 1.1, 2.2, 3.3, 4.4, "hello", "goodbye" };
auto length = size(original);
std::vector<unsigned char> buffer(length);
write(buffer.data(), original);
Structure copy;
read(buffer.data(), copy);
print_tuple(original);
print_tuple(copy);
return 0;
}
Just add the first argument in the apply function:
Demo
That assumes that tuple_size is not 0.
If you have to handle the case of 0, overloaded (from
std::visit
example) might helpand then