TL;DR
I have a struct with more than just member variables (e.g. they contain functions), and I want to convert only the member variables into a byte array(/vector), such that I can upload data to the graphics card in Vulkan. How do I get only that part of the struct, which represents the member variables?
My concrete approach
I have a setup where I use an empty ParamsBase
struct and from that inherit ParamsA
, ParamsB
... structs which actually contain member variables. I use this such that I can keep members of ParamsBase
in a Container
class without actual knowledge of the concrete implementation.
I want to turn the Params instance in the Container
class into a byte buffer.
Since I need the size of the actual ParamsA
/ParamsB
/... struct, I have used a generic subclass for these when creating instances that allows me to use a single getSize()
function, rather than overwriting this in every substruct.
// =============================
// === Define Params structs ===
// =============================
struct ParamsBase
{
virtual size_t getSize() const noexcept = 0;
};
struct ParamsA : public ParamsBase
{
float vec[3] = { 2.3f, 3.4f, 4.5f };
};
// ===============================================================
// === enable sizeof query for derived structs from ParamsBase ===
// ===============================================================
template<class T>
struct GetSizeImpl : T
{
using T::T;
size_t getSize() const noexcept final { return sizeof(T); }
};
template<class T, class... Args>
std::unique_ptr<T> create(Args&&... args)
{
return std::unique_ptr<T>(new GetSizeImpl<T>(std::forward<Args>(args)...));
}
// ============
// === Util ===
// ============
template<typename T>
std::vector<uint8_t> asByteVector(T* t, size_t size)
{
std::vector<uint8_t> byteVec;
byteVec.reserve(size);
uint8_t* dataArr = std::bit_cast<uint8_t*>(t);
byteVec.insert(byteVec.end(), &dataArr[0], &dataArr[size]);
return byteVec;
}
// ============================================
// === Use Params struct in container class ===
// ============================================
class Container
{
public:
Container(ParamsBase* params) : Params(params) {}
const std::vector<uint8_t>& getParamsAsBuffer()
{
ParamsAsBuffer = asByteVector(Params, Params->getSize());
return ParamsAsBuffer;
}
size_t getParamsSize() const { return Params->getSize(); }
private:
ParamsBase* Params = nullptr;
std::vector<uint8_t> ParamsAsBuffer;
};
Using all this, the sizes of my Params structs is too large and starts of with two bytes containing some garbage(?) data. I assume it has to do with the getSize()
function, since even a non-templated struct with a function has this problem, but I cannot be sure if this assumption is correct.
This little comparison shows differences between what I get and what I want:
// ================================
// === Define comparison struct ===
// ================================
struct ParamsCompareA
{
float vec[3] = { 2.3f, 3.4f, 4.5f };
};
int main()
{
// create instances
auto pA = create<ParamsA>();
Container cA(pA.get());
std::vector<uint8_t> vecA = cA.getParamsAsBuffer();
// comparison
ParamsCompareA pcA;
size_t sizeCompA = sizeof(ParamsCompareA);
std::vector<uint8_t> compVecA = asByteVector(&pcA, sizeof(ParamsCompareA));
std::cout << "ParamsA size: " << std::to_string(pA->getSize())
<< "; CompParamsA size: " << sizeof(ParamsCompareA) << std::endl;
float* bufAf = reinterpret_cast<float*>(vecA.data());
float* bufCompAf = reinterpret_cast<float*>(compVecA.data());
}
The compVecA
contains 12 entries (i.e. the struct is 12 bytes large), reinterpreting them as floats shows the correct values. The vecA
is reported to have 24 entries and the actual data I want is at (0 based) bytes 2 to 14.
I could hardcode an offset of 2 (which is the same as sizeof(GetSizeImpl)
), but I'm pretty sure this is not the correct way to handle this. So, is there a way I can get to only the data part I want?
The purpose of the Param sub structs is to make it as easy as possible for a user to add their own Parameter struct and upload it to a Vulkan Buffer (/Shader), i.e. they should worry only about the data they actually need and I can do all the handling and conversion elsewhere.
sizeof
of the object. If you want to use polymorphism and virtual functions, you can't just do a raw byte-wise copy an object into a byte array. You need to serialize the data. – Bolerostd::memcpy
your struct, then just make it POD and define methods manipulating is somewhere else: in another struct by composition, or as free functions. – Cipolintruct GetSizeImpl : T
is both overcomplicated and wrong - because it contains vtable.. Just impl virtualstd::vector<uint8_t> Params::serialize();
functions and you are good to go. It can be trivialmemcpy
of members, or something more sophisticated – Cipolinstd::start_lifetime_as_array
to allowbufAf
to be read from. – Rocky