1

My goal is to have a memory pool non-template class that is used to store arrays of objects. The same memory pool object must be reusable for a different array (difference size, different type and/or alignement).

I already posted a series of questions but they might have been too focused on technicals details about a possible implementation, while this implementation might not be the correct one:

I will, with this question, focus on the "what".
I'd like to have a memory-pool class with this pseudo-code API (and an usage example):

// type-punning reusable buffer for arrays
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
    // start of storage address
    char* p = nullptr;

    // adding whatever method and variable required to make it work
    // ...

    // Creates an adequate storage (if needed) to store an array of N object of
    // type T and default-construct them returns a pointer to the first element
    // of this array
    template <typename T>
    T* DefaultAllocate(const size_t N);
    // Ends lifetime of the currently stored array of objects, if any, leaving
    // the storage reusable for another array of possibly different type and
    // size
    // Make it non-template if possible
    // Make it optional if possible (by calling it automatically in
    // DefaultAllocate if needed)
    template <typename T>
    void Deallocate() {}
    // Releasing all ressources (storage and objects)
    ~Buffer() {}
};

int main() {
    constexpr std::size_t N0 = 7;
    constexpr std::size_t N1 = 3;
    Buffer B;
    std::cout << "Test on SomeClass\n";
    SomeClass* psc = B.DefaultAllocate<SomeClass>(N0);
    psc[0] = somevalue0;
    *(psc + 1) = somevalue1;
    psc[2] = somevalue2;
    std::cout << psc[0] << '\n';
    std::cout << psc[1] << '\n';
    std::cout << *(psc + 2) << '\n';
    std::cout << "Test on SomeOtherClass\n";
    // reallocating, possibly using existing storage, for a different type and
    // size
    SomeOtherClass* posc = B.DefaultAllocate<SomeOtherClass>(N1);
    std::cout << posc[0] << '\n';
    std::cout << posc[1] << '\n';
    std::cout << posc[2] << '\n';
    return 0;
}

Editable version in compiler explorer.

How should be implemented this class to avoid UB, memory leaks, to let pointer arithmetic be valid on the typed pointer (the one returned by ``DefaultAllocate```) and have proper alignment?

I'm expecting C++14 answers with technical references and explanations (what ensures the absence of UB, the validity of pointer arithmetic,...).

But I'm also interested in how to do that in more modern versions (especially as there has been some fundamental changes that lead to the need of std::launder in some specific situations).

NB In type-punning with std::aligned_alloc for array of objects, a very interesting technics (using std::function and lambdas has been proposed in order to help data erasing).

2
  • Shouldn't Buffer just be an Allocator, and use/pass it in container such as std::vector? Commented Aug 25, 2023 at 9:17
  • @Jarod42 I'm not sure. First I don't know how to implement a proper allocator, first my need is to pass around a non typed-buffer (actually, I should update my API to add a template function that returns the pointer (with or without type checking). In some way,it can be seen as an any class, for arrays and with reusable storage. Commented Aug 25, 2023 at 9:56

2 Answers 2

2

Firstly:

  • You can only accept types that are std::is_trivially_destructible, if you want to clean the pool simply by deallocating the memory block.

  • Ensuring correct alignment is trivial, that's left as an exercise to the reader.


Creating a single object

T *MakeOne()
{
    return ::new(static_cast<void *>(address)) T{};
}

Adding {} makes it zero some types that would otherwise be uninitialized: scalars, and classes that have an implicitly generated default constructor, or a default constructor marked =default directly in the class body (for classes, only members that are otherwise uninitialized are zeroed).

Adding :: and static_cast<void *>(...) makes sure this always picks the builtin placement-new, as opposed to some user-provided overload.

Creating an array in C++20

T *MakeArray(std::size_t n)
{
    return ::new(static_cast<void *>(address)) T[n]{};
}

Creating an array before C++20

(the only compiler that required this workaround was MSVC, see Microsoft C/C++ language conformance, and see CWG2382 Array allocation overhead for non-allocating placement new)

T *MakeArray(std::size_t n)
{
    for (std::size_t i = 0; i < n; i++)
        ::new(static_cast<void *>(address + i * sizeof(T))) T{};
    // Just remove `launder` if your language standard version doesn't have it.
    return std::launder(reinterpret_cast<T *>(address));
}

std::launder is required when you have a pointer to a memory location containing an object, but the pointer was obtained in an illegal way (the standard says it "doesn't point to" your object, despite having the right value), such as when you didn't store the value returned by placement-new, and only know the pointer that was originally passed to it.

The lack of std::launder very rarely breaks things in practice (std::vector couldn't be implemented without UB before C++17 added launder, and nobody had issues with it). So if you're using C++14 or earlier, you can just omit it and things should work.

More UB could be hiding here, such as using + on a pointer to raw storage (see this related question), but this is arguably a defect in the standard, and no compiler enforces this.

Sign up to request clarification or add additional context in comments.

14 Comments

"std::vector couldn't be implemented without UB before C++17". In fact, it is pointer arithmetic (data()) which (would) make std::vector UB (but as part of std library, rules are different). Without std::launder, we might have an extra array of pointer to store the pointer returned by placement new (inefficient but legal)...
@HolyBlackCat 1- I'm not sure how to properly get the right alignment (I will do through allocating an extra sizeof(T) bytes and computing an offset in order to fix any misalignment) 2- you're using placement new for each individual object but is it sufficient to guarantee that pointer arithmetic is valid. I thought that a proper array object is required and I don't know if a juxtaposition of objects is actually constituing an array in C++ sense. 3- why could I not destroy individually each object after usage for using non trivially destructible classes? TBC...
@HolyBlackCat wouldn't it be UB to re-place objects of new types on top of the previous one without having properly ended there lifetime (through destruction for instance, see 3)? I'm far from being a grammar layer and the standard is very confusing for me with respect to type-punning, aliasing,...
NB I proposed a code on codereview that sums up many idea gathered in this question and the related ones.
@JanSchultke The issue I'm solving here is that array-new used to be allowed to consume more memory than sizeof(T)*N by adding arbitrary metadata at the beginning of the supplied memory block (which is complete nonsense, of course, and only MSVC did that). This made it effectively unusable, since one can't portably predict how much extra memory you need to allocate (and one wouldn't want to waste that memory in the first).
|
-1

You can create 'template class' and do something like this:

    [[nodiscard]] T* allocate(std::size_t n)  noexcept
    {
       
        if (auto p = static_cast<T*>malloc(n * sizeof(T))))
        {
            return p;
        }
        return nullptr;
    }

  
    void deallocate(T* p, std::size_t n) noexcept
    {
        if( nullptr != p )
            free(p);
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.