15

I have some code that look like this:

template<typename T>
struct memory_block {
    // Very not copiable, this class cannot move
    memory_block(memory_block const&) = delete;
    memory_block(memory_block const&&) = delete;
    memory_block(memory_block&) = delete;
    memory_block(memory_block&&) = delete;
    memory_block& operator=(memory_block const&) = delete;
    memory_block& operator=(memory_block&&) = delete;

    // The only constructor construct the `data` member with args
    template<typename... Args>
    explicit memory_block(Args&&... args) noexcept :
        data{std::forward<Args>(args)...} {}

    T data;
};

template<typename T>
struct special_block : memory_block<T> {
    using memory_block<T>::memory_block;
    std::vector<double> special_data;
};

// There is no other inheritance. The hierarchy ends here.

Now I have to store these types into type erased storage. I chose a vector of void* as my container. I insert pointers of the data member into the vector:

struct NonTrivial { virtual ~NonTrivial() {} };

// exposed to other code
std::vector<void*> vec;

// My code use dynamic memory instead of static
// Data, but it's simpler to show it that way.
static memory_block<int> data0;
static special_block<NonTrivial> data1;

void add_stuff_into_vec() {
    // Add pointer to `data` member to the vector.
    vec.emplace_back(&(data0->data));
    vec.emplace_back(&(data1->data));
}

Then later in the code, I access the data:

// Yay everything is fine, I cast the void* to it original type
int* data1 = static_cast<int*>(vec[0]);
NonTrivial* data1 = static_cast<NonTrivial*>(vec[1]);

The problem is that I want to access special_data in the non-trivial case:

// Pretty sure this cast is valid! (famous last words)
std::vector<double>* special = static_cast<special_block<NonTrivial>*>(
    static_cast<memory_block<NonTrivial>*>(vec[1]) // (1)
);

So now, the question

The problem arise at line (1): I have a pointer to data (of type NonTrivial), which is a member of memory_block<NonTrivial>. I know that the void* will always point to the first data member of a memory_block<T>.

So is casting a void* to the first member of a class into the class safe? If not, is there another way to do it? If it can make things simpler, I can get rid of the inheritance.

Also, I have no problem using std::aligned_storage in this case. If that can solve the problem, I'll use that.

I hoped standard layout would help me in this case, but my static assert seem to fail.

My static assert:

static_assert(
    std::is_standard_layout<special_block<NonTrivial>>::value,
    "Not standard layout don't assume anything about the layout"
);
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Well technically I believe the compiler decides where to put member variables and how to align them. I'm not sure if you could assume it is a guarantee. You could add a `static_assert` that checks `sizeof(memory_block) == sizeof(T)` that could give you a guarantee :-) – Neijwiert May 02 '19 at 14:00
  • @Neijwiert yes and no. There is some guarantees the stardard make. – Guillaume Racicot May 02 '19 at 14:01
  • Your only problem is knowing you actually have a pointer to a specific derived type. – Deduplicator May 02 '19 at 14:03
  • @Deduplicator Why is it a problem? In my code I know when I have that specific derived type, and it's when `std::is_polymorphic::value` is true. – Guillaume Racicot May 02 '19 at 14:05
  • 3
    For standard layout types the first member is the address of the class. After that we really have no other guarantees except that members are laid out in memory in the order they are declared, but there can/will be padding between them. – NathanOliver May 02 '19 at 14:05
  • @GuillaumeRacicot That only works on compile-time information, specifically the static type. Your problem is that only some of your `T`s are polymorphic, and they aren't even in the same polymorphic hierarchy. – Deduplicator May 02 '19 at 14:08
  • 1
    @FrançoisAndrieux The type itself isn't polymorphic, only a member is. Sometimes. – Deduplicator May 02 '19 at 14:09
  • 1
    If you can upgrade to C++17 you could use `std::any` and the visitor pattern with `std::visit` to handle the type erasure and access for you. – NathanOliver May 02 '19 at 14:14
  • As far as I understand, `special_block` is irrelevant to the issue - the contentious pointer cast (from member to enclosing class) already is impossible from `NonTrivial*` to `memory_block*` (whereas going from `memory_block*` to `special_block*` is totally fine if that's what the object actually is). – Max Langhof May 02 '19 at 14:19
  • @MaxLanghof I though so but still wasn't exactly sure. – Guillaume Racicot May 02 '19 at 14:20
  • @NathanOliver The types I contain sometimes are not copiable and/or not moveable. Even if I upgraded I don't think `std::any` is a good fit. – Guillaume Racicot May 02 '19 at 14:24

1 Answers1

15

As long as memory_block<T> is a standard-layout type [class.prop]/3, the address of a memory_block<T> and the address of its first member data are pointer interconvertible [basic.compound]/4.3. If this is the case, the standard guarantees that you can reinterpret_cast to get a pointer to one from a pointer to the other. As soon as you don't have a standard-layout type, there is no such guarantee.

For your particular case, memory_block<T> will be standard-layout as long as T is standard-layout. Your special_block will never be standard layout because it contains an std::vector (as also pointed out by @NathanOliver in his comment below), which is not guaranteed to be standard layout. In your case, since you just insert a pointer to the data member of the memory_block<T> subobject of your special_block<T>, you could still make that work as long as T is standard-layout if you reinterpret_cast your void* back to memory_block<T>* and then static_cast that to special_block<T>* (assuming that you know for sure that the dynamic type of the complete object is actually special_block<T>). Unfortunately, as soon as NonTrivial enters the picture, all bets are off because NonTrivial has a virtual method and, thus, is not standard layout which also means that memory_block<NonTrivial> will not be standard layout…

One thing you could do is, e.g., have just a buffer to provide storage for a T in your memory_block and then construct the actual T inside the storage of data via placement new. for example:

#include <utility>
#include <new>

template <typename T>
struct memory_block
{
    alignas(T) char data[sizeof(T)];

    template <typename... Args>
    explicit memory_block(Args&&... args) noexcept(noexcept(new (data) T(std::forward<Args>(args)...)))
    {
        new (data) T(std::forward<Args>(args)...);
    }

    ~memory_block()
    {
        std::launder(reinterpret_cast<T*>(data))->~T();
    }

    …
};

That way memory_block<T> will always be standard-layout…

Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • I made some tests and it seem `memory_block` is okay, but not `special_block`. Am I still okay if I cast the pointer to a `memory_block` before? – Guillaume Racicot May 02 '19 at 14:10
  • 1
    Does this mean you would have to first `static_cast(vec[1])` before you then `reinterpret_cast*>`? – Max Langhof May 02 '19 at 14:10
  • 1
    @GuillaumeRacicot `memory_block` is only standard layout iff `T` is standard layout. Since `NonTrivial` is not standard layout (it has a virtual function), casting from `NonTrivial*` to `memory_block*` is not allowed. Going from `memory_block*` to `special_block*` is not relevant to the issue. Or at least I don't see how. – Max Langhof May 02 '19 at 14:11
  • Might also want to point out that `special_block` is never standard layout since it contains a `vector`. – NathanOliver May 02 '19 at 14:15
  • 1
    @NathanOliver And because it has non-static members both in itself and in its base class. – Max Langhof May 02 '19 at 14:15
  • @GuillaumeRacicot I must've missed something when initially reading your question or something must've changed while I wrote my answer. I updated my answer a bit. I think you could make this work even for the `NonTrivial` case by emplacing the `T` in `memory_block` rather than having it a member directly… – Michael Kenzel May 02 '19 at 14:36
  • @MichaelKenzel This is exactly what I needed. Thanks for the updated answer! I think I will mention that part of the case in the question. – Guillaume Racicot May 02 '19 at 14:37
  • @GuillaumeRacicot updated once more with a bit more code… – Michael Kenzel May 02 '19 at 14:41
  • Is `(T*)(void*)&x.data` legal in the end tho? With a launder; but then can you get back to a pointer to the block? I'm uncertain. – Yakk - Adam Nevraumont May 05 '19 at 03:19