Improve this std::index_sequence idiom code

78 views Asked by At

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;
}
1

There are 1 answers

1
Jarod42 On BEST ANSWER

Just add the first argument in the apply function:

// READ
template<class T>
constexpr void read_data(const unsigned char* &data, T&& val) {
    using Type = std::decay_t<T>;
    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<class T>
constexpr void read(const unsigned char* data, T&& object) {
    std::apply(
        [&](auto type, auto&&... args) {
//          ^^^^^^^^^
            data += sizeof(decltype(type)); // header
            (read_data(data, args), ...); },
        std::forward<T>(object).as_tuple());
}

Demo

That assumes that tuple_size is not 0.

If you have to handle the case of 0, overloaded (from std::visit example) might help

// helper type for the visitor #4
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

and then

template<class T>
constexpr void read(const unsigned char* data, T&& object) {
    std::apply(
        overloaded(
            [](){ /* Empty pack code */ },
            [&](auto type, auto&&... args) { /*above code*/ }),
        std::forward<T>(object).as_tuple());
}