3

There is already a lot of posts about strict aliasing rule and type-punning but I couldn't find an explanation that I could understand regarding array of objects. My goal is to have a memory pool non-template class that is used to store arrays of objects. Basically I need to know the actual type only at access time: it can be seen as a non template vector whose iterators would be template. The design I thought of rises several questions so I will try to split them into several SO questions.

My question (which is the third one, see below) is merely how to implement the DefaultAllocate() function in C++17 with std::aligned_alloc (and without std::align_storage which is doomed to deprecation).

#include <cassert>
#include <iostream>
#include <type_traits>

// type that support initialisation from a single double value
using test_t = float;

// just for the sake of the example: p points to at least a sequence of 3 test_t
void load(test_t* p) {
    std::cout << "starting load\n";
    p[0] = static_cast<test_t>(3.14);
    p[1] = static_cast<test_t>(31.4);
    p[2] = static_cast<test_t>(314.);
    std::cout << "ending load\n";
}

// type-punning buffer
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
    // buffer address
    char* p = nullptr;
    // number of stored elements
    size_t n = 0;
    // buffer size in bytes
    size_t s = 0;
    // allocates a char buffer large enough for N object of type T and
    // default-construct them
    // calling it on a previously allocated buffer without adequate call to
    // Deallocate is UB
    template <typename T>
    T* DefaultAllocate(const size_t N) {
        size_t RequiredSize =
            sizeof(std::aligned_storage_t<sizeof(T), alignof(T)>) * N;
        n = N;
        T* tmp;
        if (s < RequiredSize) {
            if (p) {
                delete[] p;
            }
            s = RequiredSize;
            std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
            p = new char[s];
            // placement array default construction
            tmp = new (p) T[N];
            // T* tmp = reinterpret_cast<T*>(p);
            // // optional for arithmetic types and also for trivially
            // destructible
            // // types when we don't care about default values
            // for (size_t i = 0; i < n; ++i) {
            //     new (tmp + i) T();
            // }
        } else {
            // placement array default construction
            tmp = new (p) T[N];
            // T* tmp = reinterpret_cast<T*>(p);
            // // optional for arithmetic types and also for trivially
            // destructible
            // // types when we don't care about default values
            // for (size_t i = 0; i < n; ++i) {
            //     new (tmp + i) T();
            // }
        }
        return tmp;
    }
    // deallocate objects in buffer but not the buffer itself
    template <typename T>
    void Deallocate() {
        T* tmp = reinterpret_cast<T*>(p);
        // Delete elements in reverse order of creation
        // optional for default destructible types
        for (size_t i = 0; i < n; ++i) {
            tmp[n - 1 - i].~T();
        }
        n = 0;
    }
    ~Buffer() {
        if (p) {
            delete[] p;
        }
    }
};

int main() {
    constexpr std::size_t N = 3;
    Buffer B;
    test_t* fb = B.DefaultAllocate<test_t>(N);
    load(fb);
    std::cout << fb[0] << '\n';
    std::cout << fb[1] << '\n';
    std::cout << fb[2] << '\n';
    std::cout << alignof(test_t) << '\t' << sizeof(test_t) << '\n';
    B.Deallocate<test_t>();
    return 0;
}

Live
Live more complex

link to question 1
link to question 2

[EDIT] this answer to this question shows that my C++14 snippet above might not be properly aligned: here is a proposed better version inspired from the referenced answer.
see also question 1 for some additional material.

Oersted
  • 769
  • 16
  • 1
    "_The third one_": You should mention that this is the question you are asking in _this_ one. (The same applies to your other questions.) – user17732522 Aug 21 '23 at 15:10
  • `aligned_alloc` is not supposed to be deprecated(contrary to `aligned_storage`). Just pass in the alignment (as first argument) and size - both in bytes. The result is aligned. https://en.cppreference.com/w/cpp/memory/c/aligned_alloc – Red.Wave Aug 21 '23 at 16:13

2 Answers2

3

how to implement the DefaultAllocate() function in C++17 with std::aligned_alloc

  • First, I'd make sure that the allocator actually releases memory when it goes out of scope. For that, I'd use a std::function<void()> in which you can store the loop calling the destructors for elements stored in the Buffer. This has the benefit that you don't need to know the type when you later want to deallocate. It's stored in the functor.
  • When checking if we need to allocate new memory we need to ...
    • check if the currently allocated memory is aligned according to alignof(T). If it's not, std::free the old memory and call std::aligned_alloc.
    • check if the the previously allocated memory is large enough. If it's not, use aligned_realloc (see Note below) to acquire more.

It could look like this:

// holds a non-typed buffer (actually a void*) that can be used to store any
// types, according to user needs
struct Buffer {
    // destructing functor storage
    std::function<void()> destructors = [] {};
    // buffer address
    void* p = nullptr;
    // number of stored elements
    size_t n = 0;
    // buffer size in bytes
    size_t s = 0;
    // allocates a buffer large enough for N objects of type T and
    // default-construct them calling it on a previously allocated buffer
    // without adequate call to Deallocate is **OK**.
    template <typename T>
    T* DefaultAllocate(const size_t N) {
        destructors(); // call destructors since it may already be occupied
        size_t RequiredSize = N * sizeof(T);
        n = N;
        T* tmp;
        if(!p || reinterpret_cast<std::uintptr_t>(p) % alignof(T) != 0) {
            // first allocation or incorrect alignment
            std::free(p);
            p = std::aligned_alloc(alignof(T), RequiredSize);
            if(p == nullptr) return nullptr;
            s = RequiredSize;
        } else if (s < RequiredSize) {
            // not enough room
            auto re = aligned_realloc(p, alignof(T), RequiredSize); // See note
            if(re == nullptr) return nullptr;
            p = re;
            s = RequiredSize;
        }
        // placement array default construction
        tmp = new (p) T[N];

        // create destructor functor
        destructors = [this] {
            T* tmp = reinterpret_cast<T*>(p);
            // Delete elements in reverse order of creation
            while (n > 0) {
                tmp[--n].~T();
            }
        };
        return tmp;
    }
    // deallocate objects in buffer but not the buffer itself
    void Deallocate() { destructors(); }

    ~Buffer() {
        destructors();
        std::free(p);
    }
};

Demo - which uses std::realloc (which has undefined behavior since the memory wasn't allocated with std::malloc, std::calloc or std::realloc). It's a conceptual demo, not a plug-and-play solution.

Note: aligned_realloc doesn't currently exist in the standard library and implementations differ when it comes to the support for doing aligned reallocation. MSVC has _aligned_realloc which requires _aligned_free and also _aligned_alloc instead of std::aligned_alloc. POSIX has posix_memalign.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • Thanks for your answer. By the way, it shows that my c++14 version is incorrect because it is misaligned. I will update my questions snippet accordingly – Oersted Aug 22 '23 at 07:43
  • perhaps I have validated the answer a bit lightly: in the realloc case, you may lose alignment, may you not? – Oersted Aug 22 '23 at 09:16
  • see: https://stackoverflow.com/questions/51876847/whats-the-reallocation-equivalent-of-stdaligned-alloc and https://stackoverflow.com/questions/64884745/is-there-a-linux-equivalent-of-aligned-realloc for instance. IMHO it's a bit strange... – Oersted Aug 22 '23 at 09:20
  • @Oersted _"in the realloc case, you may lose alignment"_ - Yes, I had that thought too when going to bed and I tried it out this morning. In the quick test, gcc lost alignment while clang did not - but it was a one shot test so perhaps clang will too. I think an `aligned_realloc` is needed and I made a quick version of it but haven't had the time to update the answer. I will do that when I get back from work (~6 hours). – Ted Lyngmo Aug 22 '23 at 09:34
  • FYI, the second link in comment propose an aligned_realloc which try a simple realooc, test aligment, and in case of failure deallocate and allocate again with proper alignment and size. – Oersted Aug 22 '23 at 09:37
  • 1
    @Oersted Yes, I had something similar in mind. Alt., keep two pointers. One aligned and one to the raw storage. That requires overallocation to work though, so perhsps not. Will think... – Ted Lyngmo Aug 22 '23 at 10:10
  • I just thought about another solution: allocate one extra-element in size and compute the smallest offset from start in order to get a valid address in terms of alignment. Just store the offset for the sake of the ```Destructors``` functor. *Et voila* – Oersted Aug 22 '23 at 11:35
  • a possible [quick and dirty implem](https://godbolt.org/z/z9zr8sW48) of a well-behaved reallocation – Oersted Aug 22 '23 at 11:41
  • @Oersted _"allocate one extra-element"_ - That's the overallocation I was thinking of. You'd then need to keep two pointers. One to the raw storage that you use with `free` and when calculating the proper alignment for the type - and one that points to the aligned offset from the raw storage pointer. I just had a quick look at your impl. and it looks like what I had in mind, yes :-) Perhaps I should just replace `std::realloc` with `aligned_realloc` and a note, leaving it up to whomever is reading this answer to make the implementation. In MSVC, `_aligned_realloc` can be used etc. – Ted Lyngmo Aug 22 '23 at 12:01
  • 1
    OK, I misunderstood your original comment. – Oersted Aug 22 '23 at 12:18
  • @Oersted No worries. Instead of providing a full fledged solution for `aligned_realloc` I added some notes. The portable and easy solution is of course to not even try to `realloc` but to `free` and `malloc` if `s < RequiredSize`. At least as a first step. – Ted Lyngmo Aug 22 '23 at 15:05
  • @Oersted It should have been _"to `free` and `aligned_alloc`"_ above of course. A sidenote: I'm comparing what cppreference says about the core C functions and what the C23 draft says about some of the new functions. Sometimes cppreference says the opposite of what the standard says so now I'm editing on cppreference a bit :-) – Ted Lyngmo Aug 22 '23 at 15:58
0

[H]ow to implement the DefaultAllocate() function in C++17 with std::aligned_alloc (and without std::align_storage which is doomed to deprecation)[?]

You can ask the compiler, no Standard Library trait actually needed ;)

template<class T>
struct RequiredSize
{
    alignas(alignof(T)) unsigned char data[sizeof(T)];
};


struct Buffer {
    // ...

    template <typename T>
    T* DefaultAllocate(const size_t N) {
        size_t RequiredSize = sizeof(RequiredSize<T>) * N;
        // ...
    }
};
YSC
  • 38,212
  • 9
  • 96
  • 149