How to get the address/offset of the struct member for serialization purposes

167 Views Asked by At

I am trying to implement a function that sets a struct member and also serializes the data to an external output. For serialization purposes, I need to know the size and offset of the struct member, but I am struggling to obtain the offset.

In the following code, when I print the value of member, I get correct values, but I'm not able to convert it to integer.

Compiler Explorer

#include <stdint.h>
#include <stddef.h>
#include <stdio.h>

struct Data {
    int32_t i;
    uint32_t u;
    bool b;
    char c;
};

Data g_data;

template <typename Type>
void set(Type Data::*member, const Type& value)
{
    // Works for printing
    printf("Size: %lu\tAddress = %lu\n", sizeof(Type), member);

    // Invalid cast error
    // printf("Size: %lu\tAddress = %lu\n", sizeof(Type), reinterpret_cast<uintptr_t>(member));

    // Works, but unnecesarily complex?
    // printf("Size: %lu\tAddress = %lu\n", sizeof(Type), reinterpret_cast<uintptr_t>(&(g_data.*member)) - reinterpret_cast<uintptr_t>(&g_data));

    g_data.*member = value;
}

int main() {
    set(&Data::i, static_cast<int32_t>(1));
    set(&Data::u, static_cast<uint32_t>(2U));
    set(&Data::b, true);
    set(&Data::c, '1');

    return 0;
}
1

There are 1 best solutions below

4
Jan Schultke On BEST ANSWER

To obtain the offset of a data member in a class type, you have two options:

offsetof

// in <cstddef>
#define offsetof(type, member) /* implementation-defined */

This macro expands to an integral constant expression of type std::size_t and gives us the offset in bytes from the start of the type. This is exactly what you want.

However, to use it, set would need to take the offset instead of the pointer to the data member:

template <typename T>
void set(std::size_t offset, const T& value)
{
    std::printf("Size: %zu\tAddress = %zu\n", sizeof(T), offset);
    // may require std::addressof to be safe against overloads of &
    std::memcpy(reinterpret_cast<char*>(&g_data), &value, sizeof(value));
}

int main()
{
    set(offsetof(Data, i), std::int32_t{1});
    set(offsetof(Data, u), std::uint32_t{2});
    set(offsetof(Data, b), true);
    set(offsetof(Data, c), '1');
}

Note 1: the proper format specifier for std::size_t is %zu, not %lu.

Note 2: offsetof requires Data to be a standard layout type, and writing to its bytes like this requires it to be a trivially copyable type. For a simple struct like yours, this is the case.

std::bit_cast

std::bit_cast is a C++20 function which takes the bytes of one object, and converts it to another object with the same bytes, but different type. Since pointers to data members are typically just an offset under the hood, this would work:

template <typename T>
void set(Type Data::*member, const T& value)
{
    std::printf("Size: %zu\tAddress = %zu\n",
                sizeof(T),
                std::bit_cast<std::size_t>(member));
    g_data.*member = value;
}

int main()
{
    set(&Data::i, std::int32_t{1});
    set(&Data::u, std::uint32_t{2});
    set(&Data::b, true);
    set(&Data::c, '1');
}

However, while this technically works and is closer to your original code, do not do this. It makes too many assumptions about the implementation, namely:

  1. how pointers to data members are represented under the hood
  2. that pointers to data members have the same size as std::size_t

With some clever metaprogramming you could solve 2. by choosing an integer with matching size, but 1. is not solvable.