-1

Overal Goal

I'm writing a storage library for an embedded system with no dynamic memory allocation which handles the storage of several different types of objects in a single array / pool of static memory. The objects should behave as if they are members of each other.

The number of objects is unknown. The data comes from a structured source so it only needs to be added to the store once. Data gets added sequentially but needs to be read arbitrarily.

Specific Problem

Each object is a simple class but instances of these classes need to access the shared storage pool.

My solution

I set up an array of unions which contain the simple POD objects, all of which have similar size. These objects store their children's location within the pool as private members. The two different types grow from opposite sides so only the total number of objects is limited.

Adding to the store can be done through the main class, which needs to access these private members so the main class is declared a friend in the object class.

However, the objects can't access the main's storage pool so for now I've declared the storage pool outside the class. I don't want to store a pointer in the objects to the store because of the overhead. Only one store needs to exist at a time.

Code (for C++11)

The below is working code but it doesn't prevent the storage pool from being accessed. So is there a better way?


#include <cstdio>
#include <cstdint>

namespace Store{

const int STORE_SIZE = 512; 

// 8 bytes
class Chair {
public:
    int a;
    int n_legs;
};

// 8 bytes
class Room {
    friend class Flat; 

private:
    uint16_t chairs_start;

public:
    uint16_t number_of_chairs;
    int a;
    int b;

    // member functions    
    Chair getChair( int idx );
};

// 8 byte element of storage can be either one or the other
union Element{
    Chair chair;
    Room room;
};

union Element;
Element pool[STORE_SIZE];

Chair Room::getChair( int idx )
{
    return pool[chairs_start - idx].chair; // needs access to pool!
}

class Flat{
public:

    void addRoom() {
        Room room;
        room.chairs_start = pool_next_chair;
        pool[pool_next_room++].room = room;
    }

    void addChair() {
        Chair chair;
        chair.a = pool_next_chair; // only for testing

        pool[pool_next_chair--].chair = chair; // add chair to pool from other side

        // incremente chair count for last added room
        pool[pool_next_room - 1].room.number_of_chairs++; 
    }


    Room getRoom( int idx ){
        return pool[idx].room;
    }

private:

    uint16_t pool_next_room = 0;    
    const uint16_t pool_first_chair = STORE_SIZE - 1;   
    uint16_t pool_next_chair = pool_first_chair;    
};

}

int main() {

    Store::Flat flat;

    flat.addRoom();
    flat.addChair();
    flat.addChair();
    flat.addRoom();
    flat.addChair();

    auto room2 =  flat.getRoom(1);

    auto chair1 = room2.getChair(0);

    printf("Got chairs %d and %d\n",chair1.a, flat.getRoom(0).getChair(1).a);
    // Got chairs 509 and 510
    return 0;
}

The code works but leaves the pool accessible. In general, is there a better layout or structuring I could use which results in a similar manner of simple access like in the above main() function?

Any other code quality comments are highly welcome! Thanks a lot!

Mark Ucka
  • 56
  • 4

3 Answers3

1

I'm not sure this is what you asked for but... You want a pool object instantiated only once and only accessible by specific classes, right ?

If I correctly understood your problem, I think you should declare the pool object as a private or protected static member of a class, and declare the classes that needs to access it as friends.
This way you have the guarantee that it will be created only once and that only the allowed classes can access it.

Let me know if I have misunderstood your problem.


By the way: Chair contains 2 ints and is of size 8 (bytes). How can you pretend that Room that contains 3 ints and 2 uint16_t can have the same size ?
Room is at least of size 16 bytes. Actually, with memory alignment, it will probably be of size 20 instead.
If you move the chairs_start declaration after number_of_chairs, you will get a size of 16 as expected.


EDIT: This is an example of what I meant.

#include <array>
namespace test
{
    class Thing
    {
        // Fill the definition as you wish
    };
    class OtherThing
    {
        // Fill the definition as you wish
    };

    union Element
    {
        Thing t;
        OtherThing ot;
    };

    class Pool
    {
        friend class Thing;                               // Thing can access the private Pool::pool
        friend class OtherThing;                          // OtherThing can access the private Pool::pool

        public:
            static const size_t STORE_SIZE = 512;

        private:
            static std::array <Element, STORE_SIZE> pool; // The static pool will be instantiated only once
    };
}

Don't forget to initialize pool somewhere in the .cpp file. This may look as follows (for example):

std::array<test::Element, test::Pool::STORE_SIZE> test::Pool::pool {}; // static data member initialization
Fareanor
  • 5,900
  • 2
  • 11
  • 37
  • Ok so both Flat should be a friend of Room and the other way round as well? Classes should be each other's friends? I'll try that. Thanks for noticing the 3rd int and alignment issue, i've fixed it in the sample code I think. – Mark Ucka Sep 11 '19 at 11:13
  • @MarkUcka Not exactly, I meant for example a `class` `Pool` that contains a `private` `static` pool data member. And you declare `Chair` and `Room` and whatever that needs to access the pool data member as `friend`s of the `Pool` class. – Fareanor Sep 11 '19 at 11:16
  • @MarkUcka I added an example if it can help you. – Fareanor Sep 11 '19 at 11:33
1

If you would, you could attack the problem from another angel.

First, make a dynamic storage function. This could simply be implementing your own, perhaps primitive, allocator (which basicly means implementing your own and allocate/deallocate , see https://en.cppreference.com/w/cpp/memory/allocator).

Then you may use your allocator for any type of collection, example with std::vector:

#include <vector>

template <class T>
using myvector = std::vector<T, MyAllocator<T> >;
myvector<Room> rooms;

Besides the obvious advantage of being more generic, this has the added benefit of not using extra space for when objects are smaller than the union size.


Update: Here is an example that uses an 'arena-style' allocator to get you startet. This is on par with what the OP provided, ie. no error-checking(no buffer overrun check) and no re-use of buffer.

#include <vector>

char mem[512*10];
unsigned int current = 0;

template<class T>
struct my_allocator {
  using value_type = T;
  T* allocate( std::size_t n ){current += n; return mem + current - n;}
  void deallocate( T* p, std::size_t n ){}

};

std::vector<int, my_allocator<int> > rooms(my_allocator<int>{});

Link to godbolt

darune
  • 10,480
  • 2
  • 24
  • 62
  • And the room class would contain a member vector of chairs I suppose. Sounds very interesting but I'm going to stick with the (for me) easier to understand array approach for now. – Mark Ucka Sep 11 '19 at 11:23
  • @MarkUcka a vector is just that, a (dynamic) array – darune Sep 11 '19 at 11:26
  • @MarkUcka please see my update, do you think you would be able to use that ? – darune Sep 11 '19 at 12:49
  • yeah I think it would be a good way to go in general but my code will be used in a project without exception handling so I don't think it's the way to go. https://stackoverflow.com/a/9612884/12021380 also suggests using EASTL though. – Mark Ucka Sep 11 '19 at 13:54
  • @MarkUcka for error handling you don't have to throw exceptions you can just terminate the program instead for example. – darune Sep 12 '19 at 07:23
0

Your requirement I don't want to store a pointer in the objects to the store because of the overhead. already drives a lot the design. You have the following choices:

  1. use a global store (as you did).
  2. pass the store in parameter to add/del functions
  3. a mix of the above (using a singleton global store as default parameters)

In all cases, you should encapsulate your store in a class and have a common allocation scheme. In the current design, you cannot have more than 2 elements (because you allocate them from extremities) and the add is implemented in the object's function.

Since you are using the position in the pool to add elements, you can use a single increasing index in the pool and assume chairs are allocated just next to it.

Example:

class Store
{
public:
    static Store& globalStore(){static Store g; return g;}
public:
    Room& allocateRoom() {return store[next_available_elt++].room;}
    Chair& allocateChair() {return store[next_available_elt++].chair;}

    Chair& getChair(Room& room, size_t i)
    {
        size_t index_room = std::distance(&store[0],(Element*)&room);
        return store[index_room + i];
    }

private:
    union Element{
        Chair chair;
        Room room;
    };
    Element store[STORE_SIZE] = // init with chaining of next;
    size_t next_available_elt = 0;
};

Note that this design is brittle. You'd better add the overhead of chaining in Chair/Room and it is cleaner design but at the cost of O(N) access to chairs.

Michael Doubez
  • 5,937
  • 25
  • 39