Home > database >  Is this a correct way to store different types in the same allocation?
Is this a correct way to store different types in the same allocation?

Time:03-28

I need to allocate a chunk of memory using malloc and then store multiple values of different plain old data types in there. Is the following a correct way to do it?

#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iostream>

struct Foo {
    int32_t a;
    int32_t b;
};

struct Bar {
    int8_t a;
    int8_t b;
};

void* offset_bytewise(void* void_ptr, ptrdiff_t count) {
    return static_cast<void*>(static_cast<char*>(void_ptr)   count);
}

void test() {
    auto void_ptr = std::malloc(10);
    new (offset_bytewise(void_ptr, 0)) Foo{ 1, 2 };
    new (offset_bytewise(void_ptr, 8)) Bar{ 3, 4 };
    auto foo_ptr = static_cast<Foo*>(offset_bytewise(void_ptr, 0));
    auto bar_ptr = static_cast<Bar*>(offset_bytewise(void_ptr, 8));

    std::cout << (int)foo_ptr->a << ' ' << (int)foo_ptr->b << '\n';
    std::cout << (int)bar_ptr->a << ' ' << (int)bar_ptr->b << '\n';

    // Re-using a part of the allocation as a different type than before.
    new (offset_bytewise(void_ptr, 1)) Bar{ 5, 6 };
    bar_ptr = static_cast<Bar*>(offset_bytewise(void_ptr, 1));

    // I suppose dereferencing `foo_ptr` would be UB now?

    std::cout << (int)bar_ptr->a << ' ' << (int)bar_ptr->b << '\n';
    std::free(void_ptr);
}

CodePudding user response:

Implicit object creation will cause the allocated memory block to contain an array of char of suitable size and the pointer returned from malloc will point to the first element of that array.

(That is assuming std::malloc doesn't return a null pointer, which you should check for.)

Such an array can provide storage for other objects (although technically the standard currently says this is only true for unsigned char and std::byte, which is probably a defect).

So you are ok to create new objects in that array as you are doing.

However, when you try to obtain a pointer to the newly created object, you cannot simply cast the pointer since elements of the array are not pointer-interconvertible with the objects for which the array provides storage.

Therefore you need to apply std::launder after casting the pointer, e.g.

auto foo_ptr = std::launder(static_cast<Foo*>(offset_bytewise(void_ptr, 0)));

Alternatively you can use the result of new which is a pointer to the newly created object:

auto foo_ptr = new(offset_bytewise(void_ptr, 0)) Foo{ 1, 2 };

Also, you need to make sure that size and alignment of the types is correct, i.e. here you should add:

static_assert(alignof(Foo) <= 8);
static_assert(sizeof(Foo) <= 8);
static_assert(alignof(Bar) <= 1);
static_assert(sizeof(Bar) <= 2);

Creating the second Bar object at offset 1 will cause the lifetime of the Foo object to end, since it overlaps with the storage of the new object. Therefore foo_ptr may afterwards be used only in the way other pointers to object out-of-lifetime of an object may be used. In particular trying to access the value of a member will be UB.

  • Related