A function for block allocation
Memory alignment requirements is something often forgotten when implementing custom memory allocation strategies. Each native type has a minimum alignment required for its starting memory address, consequently, structured types will inherit the requirement from its members. std::aligned_storage and std::aligned_union are a good example on that. Also refer to: How to allocate aligned memory only using the standard library?

Operator new and malloc are guaranteed to always return a starting address that is aligned for the most demanding type available (like 16 bytes for a __m128 vector type), so memory returned by those system allocators will be suitable to store such objects and homogeneous arrays of them. The problem with mixing objects with different alignment requirements in the same memory block is that if one item or chunk of items in this mixed array breaks the alignment, whatever follows will be misaligned, so you run the risk of getting memory access errors at the CPU level when touching that memory.

Your block allocator has that problem, since it accepts generic types, so you need to fix it in order to have a robust implementation that can allocate anything. To do that, you just have to increment each pointer to an aligned address, and also add an extra overhead to the parent block, in account of that. Something like the following:

void * aligned_ptr(void * ptr, std::size_t alignment)
{
    // Cast to integer and align the address:
    std::uintptr_t uintPtr = reinterpret_cast&lt;std::uintptr_t&gt;(ptr);
    std::uintptr_t alignedPtr = (uintPtr + (alignment - 1)) &amp; ~(alignment - 1); // Power-of-two alignment assumed!

    // Re-cast to void*, validate and return:
    void * userPtr = reinterpret_cast&lt;void *&gt;(alignedPtr);
    assert(is_aligned_ptr(userPtr, alignment));
    return userPtr;
}

std::size_t aligned_size(std::size_t size, std::size_t alignment)
{
    // Add the minimum extra needed to the size for pointer alignment.
    // This size can then be used to malloc() some memory
    // and then have the pointer aligned with aligned_ptr().
    return size + (alignment - 1);
};

block block_allocate(A*&amp; pA, std::size_t nA, B*&amp; pB, std::size_t nB, C*&amp; pC, std::size_t nC)
{
    auto sizeBlockA = aligned_size(nA * sizeof(A), alignof(A));
    auto sizeBlockB = aligned_size(nB * sizeof(B), alignof(B));
    auto sizeBlockC = aligned_size(nC * sizeof(C), alignof(C));

    block blk = alloc_bytes(sizeBlockA + sizeBlockB + sizeBlockC);

    pA = reinterpret_cast&lt;A*&gt;(aligned_ptr(blk.ptr, alignof(A))); // if allocated with new, already aligned, but doesn't harm checking either...
    pB = reinterpret_cast&lt;B*&gt;(aligned_ptr(blk.ptr + sizeBlockA, alignof(B)));
    pC = reinterpret_cast&lt;C*&gt;(aligned_ptr(blk.ptr + sizeBlockA + sizeBlockB, alignof(C)));

    return blk;
}


Please note that the above example was not tested, but hopefully gives you a good idea of the procedure.



Other little things:

Instead of returning a plain unique_ptr to the memory, I'd suggest augmenting the pointer with a small structure that has some extra context, like the total size of the memory. This is oftentimes useful for debugging and profiling (the block type I invented above):

struct block
{
    std::unique_ptr&lt;std::uint8_t[]&gt; ptr;
    std::size_t size;
};


Or you might modify it to use of a std::vector. It is certainly possible, but might be overkill, since the block is not meant to be resized.

Very minor nitpicking, but notice that I use the std:: prefix in the extended integer types like size_t? That's the correct usage, since they are members of the standard namespace in C++. It works without because shared C headers expose them globally, but a pure C++ compiler is not required to provide that.