T4r4nis
T4r4nis

Reputation: 31

Custom Arena allocator raises SIGSEGV

(first thing, I'm french so sorry for the typos) I'm currently in the process of learning memory optimisation for C++ programs. I discovered the concept of Arena Allocator and tried putting one together by myself. The allocator works fine for standard types or classes without inheritance. But as soon as I try to put a derivated class in the allocater and using them, I get a Segmentation fault.

On this exemple, the SIGSEGV is raised when trying to use the "third" pointer.

So what am I doing wrong and how to fix it? (I'm using cpp14 and Code::Blocks with default compiler [GNU GCC])

main.cpp

#include "util/arena.hpp"

class Foo
{
protected:
    int m_val;
public:
    Foo(int val): m_val(val) {}
    Foo():m_val(0) {}
    virtual ~Foo() {}
    virtual int evaluate() {return m_val;}
};

class Bar: public Foo
{
protected:
    Foo* m_other;
public:
    Bar(int val, Foo* other): m_other(other) {m_val=val;}
    Bar(): m_other(nullptr) {m_val=0;}
    virtual ~Bar() {}
    virtual int evaluate() {return m_val + m_other->evaluate();}
};

int main()
{
    ArenaAllocator alloc(64);

    Foo* first = alloc.allocate_type<Foo>();
    Bar* second = alloc.allocate_type<Bar>();
    Bar* third = alloc.allocate_type<Bar>();
    *first = Foo(1);
    *second = Bar(2,first);
    *third = Bar(3,second);

    third->evaluate(); /// <-- HERE

}

util/arena.hpp

#ifndef ARENA_HPP_INCLUDED
#define ARENA_HPP_INCLUDED

#include <stdexcept>

class ArenaAllocator{

    typedef unsigned char uchar;

protected:
    size_t m_size;
    uchar* m_allocator; //uses char allocation to allow pointer arithmetic
    uchar* m_current; //(forbidden for void pointers)

public:

    /** \brief by default, the arena allocated has no data;
     */
    ArenaAllocator();

    /** \brief But you can specify that size using this constructor
     */
    ArenaAllocator(size_t toAllocate);

    /** \brief When the destructor is called, all data stored is deleted.

    */
    virtual ~ArenaAllocator();

    /** \brief Reserve a place in the arena
     *  \param toReserve: the size that the pointer should move
     *         returns a void pointer to the beginning of the reserved memory
     */
    void* allocate(size_t toReserve);

    /** \brief return a specific type of pointer with corresponding size of allocation
     *      it serves as a practical shortcut to allocate(), without specifying size of allocation
     *      or making type conversion
     *  \param number: lets you allocate multiple instances of the corresponding type. (like an array with number element)
     */
    template<class T>
    inline T* allocate_type(size_t number=1){return (T*)this->allocate(sizeof(T)*number);}

    /** \brief Moves back the top of the arena. Does not destroy data, but it can be overridden by future allocation
     *  \param toScrap: the size that the pointer should move
     */
     void backtrack(size_t toScrap);

    /** \brief totally resets the arena. The allocation is still in place (and all data is kept)
     *  but the allocation pointer is thrown back at the beginning, which mean all allocated pointer
     *  are therefore "invalid", since data can be overwritten
     */
     void clear();

     /** \brief free all data and reallocates a new segment in memory;
     */
     void reallocate(size_t toAllocate);

     /** \brief returns the size of the arena
      */
     const size_t size() const;

     /** \brief returns a void pointer on the beginning of allocated bloc of memory
      */
      const void* begin() const;

};

#endif // ARENA_HPP_INCLUDED

util/arena.cpp

#include "arena.hpp"

ArenaAllocator::ArenaAllocator(): m_size(0), m_allocator(nullptr), m_current(nullptr) {
}


ArenaAllocator::ArenaAllocator(size_t toAllocate): m_size(toAllocate){
    m_allocator = new uchar[m_size];
    m_current = m_allocator;
}


ArenaAllocator::~ArenaAllocator(){
    if (m_allocator){
        delete[] m_allocator;
    }
}


void* ArenaAllocator::allocate(size_t toReserve){
    void* allocated = m_current; //casting char pointer to void pointer so that conversion in output is easier
    m_current += toReserve;
    if (m_current>m_allocator+m_size){
        throw std::bad_alloc();
    }
    return allocated;
}


void ArenaAllocator::clear(){
    m_current = m_allocator;
}

void ArenaAllocator::backtrack(size_t toScrap){
    m_current = std::max(m_allocator, m_current-toScrap);
}

void ArenaAllocator::reallocate(size_t toAllocate){
    if (m_allocator){
        delete[] m_allocator;
    }
    m_size = toAllocate;
    m_allocator = new uchar[m_size];
    m_current = m_allocator;
}

const size_t ArenaAllocator::size() const{
    return m_size;
}

const void* ArenaAllocator::begin() const{
    return m_allocator;
}

I tried the allocator with standard types, pointers, and class with pointers and it works just fine. But as soon as I use non-base classes with virtual function members it is screwing up. I used GDB to watch data inside the allocator and pointers are valid. Objects are also correctly constructed, so I suspect it is all about the objects' vtable;

Upvotes: 1

Views: 494

Answers (1)

n. m. could be an AI
n. m. could be an AI

Reputation: 120049

A mindless, totally mechanical application of an essential tool immediately reveals the problem. The thing you think is a Foo object isn't.

Why is that?

Because of this:

`inline T* allocate_type(size_t number=1){return (T*)this->allocate(sizeof(T)*number);}` 

This function does not create a T object. It allocates raw bytes, and cast their address to T*. This does not make a T magically appear there. Any dereference of the resulting pointer is undefined behaviour.

You need to invoke a constructor. A constructor is invoked by operator new. In this case, you need placement new.

Upvotes: 1

Related Questions