At one point in my code I am required to pass some parameters as a POD struct (copy data to CUDA constant memory specifically). But I want to pass more "complex" types with user-defined constructors (no virtual methods).
I was wondering if there was any issue (or better solutions) doing something like this to alleviate a POD constraint (basically using a POD struct at least the same size as my object of interest as a proxy for the real thing).
#include <iostream>
#include <cstring>
// Meant to be used as a pointer to the Derived type.
template <class T>
struct PODWrapper
{
uint8_t data_[sizeof(T)];
const T& operator*() const { return *reinterpret_cast<const T*>(this); }
const T* operator->() const { return reinterpret_cast<const T*>(this); }
};
class NonPOD
{
protected:
float x_;
public:
NonPOD(float x) : x_(x) {}
NonPOD(const NonPOD& other) : x_(other.x_) {}
float x() const { return x_; }
float& x() { return x_; }
};
int main()
{
// initial value
NonPOD initial(10.0f);
//copying to pod wrapper
PODWrapper<NonPOD> pod;
std::memcpy(&pod, &initial, sizeof(NonPOD));
// accessing pod wrapper
NonPOD nonpod(*pod);
std::cout << nonpod.x() << std::endl;
return 0;
}
The use case is to be able to declare a struct of CUDA constant memory with any type (CUDA expects a POD type). Something like this:
__constant__ PODWrapper<NonPOD> constantData;
I tested this and it seem to work but I am especially concerned about memory issue, namely using memcpy to/from the 'this' pointer of the PODWrapper.
Your PODWrapper exhibits undefined behaviour in three ways. Here's a fix:
template <class T>
struct PODWrapper
{
alignas(T) std::byte data_[sizeof(T)];
const T& operator*() const { return *std::launder(reinterpret_cast<const T*>(data_)); }
const T* operator->() const { return std::launder(reinterpret_cast<const T*>(data_)); }
};
Without aligning your byte store you are not guaranteed to have enough memory. Furthermore you must std::launder
the memory address.
However, the biggest problem is that there is no object of type created anywhere (except for initial
). The memory is there, but in terms of C++ no NonPOD
object resides in that memory. You can use std::construct_at
and std::destory_at
to create and destroy the object.
std::construct_at(pod.data_, initial);
Note that the object is not the same as the memory where the object is stored. Especially for non-trivial types (the concept of POD is somewhat outdated and no longer applicable, btw.). See TrivialType for further information.
Do not memcopy into data_
. It will not create an object and you will still be in UB-land.
The access looks fine to me.