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.
CodePudding user response:
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.
CodePudding user response:
I'm not sure why you want to make wrapper when it is very simple to convert your "NonPOD" class to "POD" class.
Look at class properties here: https://en.cppreference.com/w/cpp/language/classes
You class lacks only 2 requierements:
- each eligible copy constructor is trivial
- has one or more eligible default constructors such that each is trivial.
To fix that, you need to remove non-trivial copy constructor (you can add default copy constructor if you want) and add default trivial constructor. I.e. this works:
#include <iostream>
#include <type_traits>
class NonPOD
{
protected:
float x_;
public:
// Add trivial constructor
NonPOD() = default;
NonPOD(float x) : x_(x) {}
// Remove non-trivial copy constructor
//NonPOD(const NonPOD& other) : x_(other.x_) {}
float x() const { return x_; }
float& x() { return x_; }
};
int main()
{
std::cout << std::boolalpha;
std::cout << std::is_trivially_copyable<NonPOD>::value << std::endl;
std::cout << std::is_standard_layout<NonPOD>::value << std::endl;
std::cout << std::is_trivial<NonPOD>::value << std::endl;
std::cout << std::is_pod<NonPOD>::value << std::endl;
}
Output is:
true
true
true
true