Search code examples
c++memorycasting

Creating a POD wrapper for non-POD types in C++


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.


Solution

  • 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.