c++optimizationmemory

How to allocate two objects of different types in a single memory block?


My goal is to allocate memory for two different types of objects, let's call them obj1 and obj2, within a single contiguous memory block. A crucial aspect is that obj1 and obj2 can be of different types (e.g., int and float, or std::complex, etc...). My aim is to achieve this with a single allocation call, conceptually similar to how std::make_shared places its managed object and control block in one contiguous chunk.

This is for performance (cache locality, reduced allocation overhead) and to gain fine-grained control over the entire memory block's lifetime, allowing for its explicit deallocation.

Example Scenario

Imagine I have a struct representing pointers to these items (though the struct itself is secondary, the primary goal is the single allocation for two items, and obtaining pointers to them for manual management):

struct DataContainer {
    int* val1;
    float* val2;
};

I need val1 to point to an int and val2 to point to a float, both located within the same memory block allocated via a single call. Crucially, I also need to retain control over when this entire block is freed.

new int[2] won't work if the types are different. Standard new calls for each type (new int; new float;) result in separate allocations, which I want to avoid to minimize overhead and fragmentation.

What I want

After a single allocation call, I should have access to two objects of specified types (e.g., int and float), guaranteed to be within the same contiguous memory block. I need the ability to explicitly manage the destruction of these objects and the deallocation of that entire block myself.

Desired Example (simplified):

void* raw_memory = allocate_two_objects_of_different_types<int, float>();

int* p_int = get_first_object_from_raw_memory<int>(raw_memory);
float* p_float = get_second_object_from_raw_memory<float>(raw_memory);

*p_int = 10;
*p_float = 20.5f;

// I also should be able do to this
deallocate_raw_memory(raw_memory);

Is there an elegant and easy way to achieve this single allocation pattern without relying on standard library containers to internally manage the allocation and deallocation of the multi-typed data?


Solution

  • One way of doing this is via std::aligned_alloc together with offsetof:

    struct Data {
      struct Glue {
        short s;
        long l;
      };
      short* ps;
      long* pl;
      void construct() {
        auto glue = static_cast<std::byte*>(std::aligned_alloc(alignof(Glue),sizeof(Glue)));
        ps = reinterpret_cast<short*>(glue+offsetof(Glue,s));
        pl = reinterpret_cast<long*>(glue+offsetof(Glue,l));
      }
      void destruct() {
        std::free(reinterpret_cast<std::byte*>(ps)-offsetof(Glue,s));
      }
    };
    

    I have used short and long instead of int and float because they actually have different alignments.

    Here is a demo on compiler explorer with ubsan and asan.

    This is only legal because Glue is trivial in every respect. This will be more complicated if any of the member types is non-trivial. In that case just allocate the memory of that size and alignment and later construct the object with placement new:

    struct Glue {
      alignas(type1) std::byte member1[sizeof(type1)];
      alignas(type2) std::byte member2[sizeof(type2)];
    };
    

    Note: You can of course do all of this without the Glue type, by adding the sizes of the individual members and ensuring they are correctly aligned. This will involve using max(alignment(member)...) and some additional arithmetic to find the correct location of the respective member addresses. Basically replace the calculation done by offsetof. More chances to make subtle mistakes.