Skip to main content
Became Hot Network Question
edited tags
Link
G. Sliepen
  • 69.5k
  • 3
  • 75
  • 180
Spelling and grammar
Source Link
Toby Speight
  • 88.7k
  • 14
  • 104
  • 327

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 alignementalignment).

Live demo I would appreciate a review on this Buffer class, in C++14. Yet I'm also interested in upgrade to C++17 and beyond.
  
I've placed // @# numbered comment in the code for focus on specific implementation issues/questions.

NBN.B. my concern in SO was that these type-punning technicstechniques, though well defined for single objects, seemed to be ill-defined when speaking of arrays.

[EDIT]Note: replacingI previously had a std::function which I replaced by a simple pointer to function. Yet I don't understand why the pointer does not getbecome dangling when leaving the DefaultAllocate function.

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).

Live demo I would appreciate a review on this Buffer class, in C++14. Yet I'm also interested in upgrade to C++17 and beyond.
  I've placed // @# numbered comment in the code for focus on specific implementation issues/questions.

NB my concern in SO was that these type-punning technics, though well defined for single objects seemed to be ill-defined when speaking of arrays.

[EDIT] replacing std::function by a simple pointer to function. Yet I don't understand why the pointer does not get dangling when leaving the DefaultAllocate function.

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 alignment).

Live demo I would appreciate a review on this Buffer class, in C++14. Yet I'm also interested in upgrade to C++17 and beyond. 
I've placed // @# numbered comment in the code for focus on specific implementation issues/questions.

N.B. my concern in SO was that these type-punning techniques, though well defined for single objects, seemed to be ill-defined when speaking of arrays.

Note: I previously had a std::function which I replaced by a simple pointer to function. Yet I don't understand why the pointer does not become dangling when leaving the DefaultAllocate function.

adding a missing variable update
Source Link
Oersted
  • 337
  • 2
  • 9
#include <cstddef>
#include <functional>
#include <iostream>

// Object constructible from a double
// forcing alignment
struct alignas(16) SFloat {
    float val = 0.f;
    SFloat() { std::cout << "Constructing a SFloat with default value\n"; };
    SFloat(const double v) : val(static_cast<float>(v)) {
        std::cout << "Constructing a SFloat from " << v << '\n';
    };
    SFloat& operator=(SFloat&& f) {
        val = f.val;
        std::cout << "Move-assigning from a SFloat " << f.val << '\n';
        return *this;
    }
    ~SFloat() { std::cout << "Destructing a SFloat holding " << val << '\n'; }
};
// Serialization of Float objects
std::ostream& operator<<(std::ostream& o, SFloat const& f) {
    return o << f.val;
}

// just for the sake of the example: p points to at least a sequence of 3 T
// probably not the best implem, but compiles without conversion warning with
// SFloat and float.
template <typename T>
void load(T* p) {
    std::cout << "starting load\n";
    p[0] = static_cast<T>(3.14);
    p[1] = static_cast<T>(31.4);
    p[2] = static_cast<T>(314.);
    std::cout << "ending load\n";
}

// type-punning reusable buffer
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
    // destructing functor storage
    // required to call the correct object destructors
    // using std::function to store a copy of the lambda used
    // @1 is there a way to avoid std::function?
    std::function<void(Buffer*)> Destructors = [](Buffer*) {};
    // buffer address
    unsigned char* p = nullptr;
    // aligned buffer address
    unsigned char* paligned = nullptr;
    // number of stored elements
    size_t n = 0;
    // buffer size in bytes
    size_t s = 0;

    // computes the smallest positive offset in bytes that must be applied to p
    // in order to have alignment a a is supposed to be a valid alignment
    std::size_t OffsetForAlignement(unsigned char const* const ptr,
                                    std::size_t a) {
        std::size_t res = reinterpret_cast<std::size_t>(ptr) % a;
        if (res) {
            return a - res;
        } else {
            return 0;
        }
    }
    // allocates a char buffer large enough for N object of type T and
    // default-construct them
    // N must be > 0
    template <typename T>
    T* DefaultAllocate(const std::size_t N) {
        // Destroy previously stored objects, supposedly ends lifetime of the
        // array object that contains them
        Destructors(this);
        std::size_t RequiredSize = sizeof(T) * N + alignof(T);
        if (s < RequiredSize) {
            std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
            // @2 creating a storage of RequiredSize bytes
            // what would be the C++17+ way of do that? std::aligned_alloc?
            p = reinterpret_cast<unsigned char*>(std::realloc(p, RequiredSize));
            s = RequiredSize;
            // here should do something for the case where p is nullptr
            paligned = p + OffsetForAlignement(p, alignof(T));
        }
        // @3 Form1 placement array new default construction: ill-defined in
        // C++14?
        // expecting starting an array object lifetime and the lifetime of
        // contained objects
        // expecting pointer arithmetic to be valid on tmp T*
        // T *tmp = new (p) T[N];
        // @4 Form2 individually creating packed object in storage
        // expecting starting an array object lifetime and the lifetime of
        // contained objects
        // expecting pointer arithmetic to be valid on tmp T*
        unsigned char* pos = paligned;
        T* tmp = reinterpret_cast<T*>(paligned);
        for (std::size_t i = 0; i < N; ++i) {
            new (pos) T();
            pos += sizeof(T);
        }

        // update nb of objects
        n = N;
        // create destructors functor
        // @5 supposedly ends the lifetime of the array object and of the
        // contained objects
        Destructors = [](Buffer* pobj) {
            T* ToDestruct = reinterpret_cast<T*>(pobj->p);
            // Delete elements in reverse order of creation
            while (pobj->n > 0) {
                --(pobj->n);
                // should be std::Destroy(ToDestruct[n]) in C++17
                // I should provide my own implementation in C++14 in order to
                // distinguish between fundamental types and other ones
                // @ how to formally en the lifetime of a fundamental objects?
                // merely rewrite on its memory location?
                ToDestruct[(pobj->n)].~T();
            }
            // @6 How to formally end the array object lifetime?
        };
        return tmp;
    }
    // deallocate objects in buffer but not the buffer itself
    // actually useless
    // template <typename T>
    // void Deallocate() {
    //     Destructors(this);
    // }
    ~Buffer() {
        // Ending objects lifetime
        Destructors(this);
        // Releasing storage
        std::free(p);
    }
};

int main() {
    constexpr std::size_t N0 = 7;
    constexpr std::size_t N1 = 3;
    Buffer B;
    std::cout << "Test on SomeClass\n";
    SFloat* ps = B.DefaultAllocate<SFloat>(N0);
    ps[0] = 3.14;
    *(ps + 1) = 31.4;
    ps[2] = 314.;
    std::cout << ps[0] << '\n';
    std::cout << ps[1] << '\n';
    std::cout << *(ps + 2) << '\n';
    std::cout << "Test on float\n";
    // reallocating, possibly using existing storage, for a different type and
    // size
    float* pf = B.DefaultAllocate<float>(N1);
    pf[0] = 3.14f;
    *(pf + 1) = 31.4f;
    pf[2] = 314.f;
    std::cout << pf[0] << '\n';
    std::cout << pf[1] << '\n';
    std::cout << *(pf + 2) << '\n';
    return 0;
}

Live demoLive demo I would appreciate a review on this Buffer class, in C++14. Yet I'm also interested in upgrade to C++17 and beyond.
I've placed // @# numbered comment in the code for focus on specific implementation issues/questions.

[EDIT] replacing std::function by a simple pointer to function. Yet I don't understand why the pointer does not get dangling when leaving the DefaultAllocate function.

#include <cstddef>
#include <functional>
#include <iostream>

// Object constructible from a double
// forcing alignment
struct alignas(16) SFloat {
    float val = 0.f;
    SFloat() { std::cout << "Constructing a SFloat with default value\n"; };
    SFloat(const double v) : val(static_cast<float>(v)) {
        std::cout << "Constructing a SFloat from " << v << '\n';
    };
    SFloat& operator=(SFloat&& f) {
        val = f.val;
        std::cout << "Move-assigning from a SFloat " << f.val << '\n';
        return *this;
    }
    ~SFloat() { std::cout << "Destructing a SFloat holding " << val << '\n'; }
};
// Serialization of Float objects
std::ostream& operator<<(std::ostream& o, SFloat const& f) {
    return o << f.val;
}

// just for the sake of the example: p points to at least a sequence of 3 T
// probably not the best implem, but compiles without conversion warning with
// SFloat and float.
template <typename T>
void load(T* p) {
    std::cout << "starting load\n";
    p[0] = static_cast<T>(3.14);
    p[1] = static_cast<T>(31.4);
    p[2] = static_cast<T>(314.);
    std::cout << "ending load\n";
}

// type-punning reusable buffer
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
    // destructing functor storage
    // required to call the correct object destructors
    // using std::function to store a copy of the lambda used
    // @1 is there a way to avoid std::function?
    std::function<void(Buffer*)> Destructors = [](Buffer*) {};
    // buffer address
    unsigned char* p = nullptr;
    // aligned buffer address
    unsigned char* paligned = nullptr;
    // number of stored elements
    size_t n = 0;
    // buffer size in bytes
    size_t s = 0;

    // computes the smallest positive offset in bytes that must be applied to p
    // in order to have alignment a a is supposed to be a valid alignment
    std::size_t OffsetForAlignement(unsigned char const* const ptr,
                                    std::size_t a) {
        std::size_t res = reinterpret_cast<std::size_t>(ptr) % a;
        if (res) {
            return a - res;
        } else {
            return 0;
        }
    }
    // allocates a char buffer large enough for N object of type T and
    // default-construct them
    // N must be > 0
    template <typename T>
    T* DefaultAllocate(const std::size_t N) {
        // Destroy previously stored objects, supposedly ends lifetime of the
        // array object that contains them
        Destructors(this);
        std::size_t RequiredSize = sizeof(T) * N + alignof(T);
        if (s < RequiredSize) {
            std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
            // @2 creating a storage of RequiredSize bytes
            // what would be the C++17+ way of do that? std::aligned_alloc?
            p = reinterpret_cast<unsigned char*>(std::realloc(p, RequiredSize));
            // here should do something for the case where p is nullptr
            paligned = p + OffsetForAlignement(p, alignof(T));
        }
        // @3 Form1 placement array new default construction: ill-defined in
        // C++14?
        // expecting starting an array object lifetime and the lifetime of
        // contained objects
        // expecting pointer arithmetic to be valid on tmp T*
        // T *tmp = new (p) T[N];
        // @4 Form2 individually creating packed object in storage
        // expecting starting an array object lifetime and the lifetime of
        // contained objects
        // expecting pointer arithmetic to be valid on tmp T*
        unsigned char* pos = paligned;
        T* tmp = reinterpret_cast<T*>(paligned);
        for (std::size_t i = 0; i < N; ++i) {
            new (pos) T();
            pos += sizeof(T);
        }

        // update nb of objects
        n = N;
        // create destructors functor
        // @5 supposedly ends the lifetime of the array object and of the
        // contained objects
        Destructors = [](Buffer* pobj) {
            T* ToDestruct = reinterpret_cast<T*>(pobj->p);
            // Delete elements in reverse order of creation
            while (pobj->n > 0) {
                --(pobj->n);
                // should be std::Destroy(ToDestruct[n]) in C++17
                // I should provide my own implementation in C++14 in order to
                // distinguish between fundamental types and other ones
                // @ how to formally en the lifetime of a fundamental objects?
                // merely rewrite on its memory location?
                ToDestruct[(pobj->n)].~T();
            }
            // @6 How to formally end the array object lifetime?
        };
        return tmp;
    }
    // deallocate objects in buffer but not the buffer itself
    // actually useless
    // template <typename T>
    // void Deallocate() {
    //     Destructors(this);
    // }
    ~Buffer() {
        // Ending objects lifetime
        Destructors(this);
        // Releasing storage
        std::free(p);
    }
};

int main() {
    constexpr std::size_t N0 = 7;
    constexpr std::size_t N1 = 3;
    Buffer B;
    std::cout << "Test on SomeClass\n";
    SFloat* ps = B.DefaultAllocate<SFloat>(N0);
    ps[0] = 3.14;
    *(ps + 1) = 31.4;
    ps[2] = 314.;
    std::cout << ps[0] << '\n';
    std::cout << ps[1] << '\n';
    std::cout << *(ps + 2) << '\n';
    std::cout << "Test on float\n";
    // reallocating, possibly using existing storage, for a different type and
    // size
    float* pf = B.DefaultAllocate<float>(N1);
    pf[0] = 3.14f;
    *(pf + 1) = 31.4f;
    pf[2] = 314.f;
    std::cout << pf[0] << '\n';
    std::cout << pf[1] << '\n';
    std::cout << *(pf + 2) << '\n';
    return 0;
}

Live demo I would appreciate a review on this Buffer class, in C++14. Yet I'm also interested in upgrade to C++17 and beyond.
I've placed // @# numbered comment in the code for focus on specific implementation issues/questions.

#include <cstddef>
#include <functional>
#include <iostream>

// Object constructible from a double
// forcing alignment
struct alignas(16) SFloat {
    float val = 0.f;
    SFloat() { std::cout << "Constructing a SFloat with default value\n"; };
    SFloat(const double v) : val(static_cast<float>(v)) {
        std::cout << "Constructing a SFloat from " << v << '\n';
    };
    SFloat& operator=(SFloat&& f) {
        val = f.val;
        std::cout << "Move-assigning from a SFloat " << f.val << '\n';
        return *this;
    }
    ~SFloat() { std::cout << "Destructing a SFloat holding " << val << '\n'; }
};
// Serialization of Float objects
std::ostream& operator<<(std::ostream& o, SFloat const& f) {
    return o << f.val;
}

// just for the sake of the example: p points to at least a sequence of 3 T
// probably not the best implem, but compiles without conversion warning with
// SFloat and float.
template <typename T>
void load(T* p) {
    std::cout << "starting load\n";
    p[0] = static_cast<T>(3.14);
    p[1] = static_cast<T>(31.4);
    p[2] = static_cast<T>(314.);
    std::cout << "ending load\n";
}

// type-punning reusable buffer
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
    // destructing functor storage
    // required to call the correct object destructors
    // using std::function to store a copy of the lambda used
    // @1 is there a way to avoid std::function?
    std::function<void(Buffer*)> Destructors = [](Buffer*) {};
    // buffer address
    unsigned char* p = nullptr;
    // aligned buffer address
    unsigned char* paligned = nullptr;
    // number of stored elements
    size_t n = 0;
    // buffer size in bytes
    size_t s = 0;

    // computes the smallest positive offset in bytes that must be applied to p
    // in order to have alignment a a is supposed to be a valid alignment
    std::size_t OffsetForAlignement(unsigned char const* const ptr,
                                    std::size_t a) {
        std::size_t res = reinterpret_cast<std::size_t>(ptr) % a;
        if (res) {
            return a - res;
        } else {
            return 0;
        }
    }
    // allocates a char buffer large enough for N object of type T and
    // default-construct them
    // N must be > 0
    template <typename T>
    T* DefaultAllocate(const std::size_t N) {
        // Destroy previously stored objects, supposedly ends lifetime of the
        // array object that contains them
        Destructors(this);
        std::size_t RequiredSize = sizeof(T) * N + alignof(T);
        if (s < RequiredSize) {
            std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
            // @2 creating a storage of RequiredSize bytes
            // what would be the C++17+ way of do that? std::aligned_alloc?
            p = reinterpret_cast<unsigned char*>(std::realloc(p, RequiredSize));
            s = RequiredSize;
            // here should do something for the case where p is nullptr
            paligned = p + OffsetForAlignement(p, alignof(T));
        }
        // @3 Form1 placement array new default construction: ill-defined in
        // C++14?
        // expecting starting an array object lifetime and the lifetime of
        // contained objects
        // expecting pointer arithmetic to be valid on tmp T*
        // T *tmp = new (p) T[N];
        // @4 Form2 individually creating packed object in storage
        // expecting starting an array object lifetime and the lifetime of
        // contained objects
        // expecting pointer arithmetic to be valid on tmp T*
        unsigned char* pos = paligned;
        T* tmp = reinterpret_cast<T*>(paligned);
        for (std::size_t i = 0; i < N; ++i) {
            new (pos) T();
            pos += sizeof(T);
        }

        // update nb of objects
        n = N;
        // create destructors functor
        // @5 supposedly ends the lifetime of the array object and of the
        // contained objects
        Destructors = [](Buffer* pobj) {
            T* ToDestruct = reinterpret_cast<T*>(pobj->p);
            // Delete elements in reverse order of creation
            while (pobj->n > 0) {
                --(pobj->n);
                // should be std::Destroy(ToDestruct[n]) in C++17
                // I should provide my own implementation in C++14 in order to
                // distinguish between fundamental types and other ones
                // @ how to formally en the lifetime of a fundamental objects?
                // merely rewrite on its memory location?
                ToDestruct[(pobj->n)].~T();
            }
            // @6 How to formally end the array object lifetime?
        };
        return tmp;
    }
    // deallocate objects in buffer but not the buffer itself
    // actually useless
    // template <typename T>
    // void Deallocate() {
    //     Destructors(this);
    // }
    ~Buffer() {
        // Ending objects lifetime
        Destructors(this);
        // Releasing storage
        std::free(p);
    }
};

int main() {
    constexpr std::size_t N0 = 7;
    constexpr std::size_t N1 = 3;
    Buffer B;
    std::cout << "Test on SomeClass\n";
    SFloat* ps = B.DefaultAllocate<SFloat>(N0);
    ps[0] = 3.14;
    *(ps + 1) = 31.4;
    ps[2] = 314.;
    std::cout << ps[0] << '\n';
    std::cout << ps[1] << '\n';
    std::cout << *(ps + 2) << '\n';
    std::cout << "Test on float\n";
    // reallocating, possibly using existing storage, for a different type and
    // size
    float* pf = B.DefaultAllocate<float>(N1);
    pf[0] = 3.14f;
    *(pf + 1) = 31.4f;
    pf[2] = 314.f;
    std::cout << pf[0] << '\n';
    std::cout << pf[1] << '\n';
    std::cout << *(pf + 2) << '\n';
    return 0;
}

Live demo I would appreciate a review on this Buffer class, in C++14. Yet I'm also interested in upgrade to C++17 and beyond.
I've placed // @# numbered comment in the code for focus on specific implementation issues/questions.

[EDIT] replacing std::function by a simple pointer to function. Yet I don't understand why the pointer does not get dangling when leaving the DefaultAllocate function.

Source Link
Oersted
  • 337
  • 2
  • 9
Loading