Smartskaft2
Smartskaft2

Reputation: 510

Framework applications using interface without heap memory allocation?

I am trying to create a framework for applications to use in my microprocessors. I am using Arduino IDE to compile and deploy the programs.

Since the microprocessors often have low heap memory, I want to only use stack memory if possible.

Minimial example:

The whole example code can be seen here.

I will describe the parts I think is most interesting.


iMinExApplication (interface):

class iMinExApplication 
{
    public:
        virtual void initialize() = 0; // pure virtual
        virtual void execute()    = 0; // pure virtual

        virtual ~iMinExApplication() = default; // Virtual destructor
};

tMinExApplication (extension of interface, only used by the framework):

class tMinExApplication
{
    public:
        ...
        tMinExApplication(iMinExApplication* app, const char name[]) : App(app)
        {
            strcpy(Name, name);
        };
        ...
        void execute()    { App->execute(); };

    private:
        iMinExApplication* App;
        char Name[32]; 
};

tMinExCoordinator (master, calling added apps)

class tMinExCoordinator
{
    public:
        ...
        void addApp(iMinExApplication* app, const char name[]) 
        {
            tMinExApplication* tmpPtr = new tMinExApplication(app, name); // HERE!
            Applications[++NumApps] = tmpPtr;
            tmpPtr = nullptr;
        };
        ...
        void runApps()
        {
            for (auto& app : Applications) {
                // Frequency check
                // ...
                app->execute();
            }
        };

    private:
        tMinExApplication* Applications[];
        int NumApps;
};

tMyApp (user defined app, using the inherited interface)

class tMyApp : public iMinExApplication {
  ...

minExSketch (Arduino IDE sketch)

#include "tMinExClasses.hpp"
#include "tMyApp.hpp"

tMinExCoordinator coordinator{};
tMyApp tmpApp{};

void setup() {
  Serial.begin(9600);
  coordinator.addApp(&tmpApp, "TEST");
  coordinator.initializeApps();
}

void loop() {
  coordinator.runApps();
}

The above works. But the apps are allocated in heap memory, since it uses the keyword new ('HERE!' in the tMinExCoordinator class definition, line 57 in tMinExClasses.hpp).

I cannot seem to get it to work without it. In what other way could I implement this, but only allocating memory in stack memory?

Requirements:

I have though of smart pointers, but am unsure if they use heap memory or not. Also, I wanted the minimal example as clean as possible.

Upvotes: 0

Views: 59

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 597941

I cannot seem to get it to work without it. In what other way could I implement this, but only allocating memory in stack memory?*

You can pre-allocate a byte array of sufficient size, and then use placement-new to construct objects inside of that array (see std::aligned_storage to help you with that). Polymorphism only requires pointers/references to work at runtime, not dynamic alllocations.

template<std::size_t MaxApps>
class tMinExCoordinator
{
    public:
        ...

        tMinExCoordinator()
        {
            Applications = reinterpret_cast<tMinExApplication*>(appBuffer);
        }

        ~tMinExCoordinator()
        {
            for (std::size_t i = 0; i < NumApps; ++i)
                Applications[i].~tMinExApplication();
        }

        void addApp(iMinExApplication* app, const char name[]) 
        {
            if (NumApps >= MaxApps)
                throw std::length_error("");

            new (&appBuffer[NumApps]) tMinExApplication(app, name);
            ++NumApps;
        }

        ...

        void runApps()
        {
            for (std::size_t i = 0; i < NumApps; ++i)
            {
                auto& app = Applications[i];
                // Frequency check
                // ...
                app.execute();
            }
        }

    private:
        typename std::aligned_storage<sizeof(tMinExApplication), alignof(tMinExApplication)>::type appBuffer[MaxApps];
        tMinExApplication* Applications;
        std::size_t NumApps = 0;
};
tMinExCoordinator<1> coordinator{};
...

The std::aligned_storage documentation linked above has an example static_vector class that uses a fixed memory buffer, which will be on the stack if the vector is constructed on the stack:

#include <iostream>
#include <type_traits>
#include <string>

template<class T, std::size_t N>
class static_vector
{
    // properly aligned uninitialized storage for N T's
    typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
    std::size_t m_size = 0;

public:
    // Create an object in aligned storage
    template<typename ...Args> void emplace_back(Args&&... args) 
    {
        if( m_size >= N ) // possible error handling
            throw std::bad_alloc{};

        // construct value in memory of aligned storage
        // using inplace operator new
        new(&data[m_size]) T(std::forward<Args>(args)...);
        ++m_size;
    }

    // Access an object in aligned storage
    const T& operator[](std::size_t pos) const 
    {
        // note: needs std::launder as of C++17
        return *reinterpret_cast<const T*>(&data[pos]);
    }

    // Delete objects from aligned storage
    ~static_vector() 
    {
        for(std::size_t pos = 0; pos < m_size; ++pos) {
            // note: needs std::launder as of C++17
            reinterpret_cast<T*>(&data[pos])->~T();
        }
    }
};

You can use that class in your coordinator, with some minor additions to it so it can work with loops, eg:

template<class T, std::size_t N>
class static_vector
{
    // properly aligned uninitialized storage for N T's
    typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
    std::size_t m_size = 0;

public:
    // Create an object in aligned storage
    template<typename ...Args> void emplace_back(Args&&... args) 
    {
        if( m_size >= N ) // possible error handling
            throw std::bad_alloc{};

        // construct value in memory of aligned storage
        // using inplace operator new
        new(&data[m_size]) T(std::forward<Args>(args)...);
        ++m_size;
    }

    // Access an object in aligned storage
    T& operator[](std::size_t pos)
    {
        // note: needs std::launder as of C++17
        return *reinterpret_cast<T*>(&data[pos]);
    }

    const T& operator[](std::size_t pos) const 
    {
        // note: needs std::launder as of C++17
        return *reinterpret_cast<const T*>(&data[pos]);
    }

    std::size_t size() const { return m_size; }
    std::size_t capacity() const { return N; }

    // iterator access to objects
    T* begin()
    {
        // note: needs std::launder as of C++17
        return reinterpret_cast<T*>(&data[0]);
    }
    T* end()
    {
        // note: needs std::launder as of C++17
        return reinterpret_cast<T*>(&data[m_size]);
    }
    const T* cbegin() const
    {
        // note: needs std::launder as of C++17
        return reinterpret_cast<const T*>(&data[0]);
    }
    const T* cend() const
    {
        // note: needs std::launder as of C++17
        return reinterpret_cast<const T*>(&data[m_size]);
    }

    // Delete objects from aligned storage
    ~static_vector() 
    {
        for(std::size_t pos = 0; pos < m_size; ++pos) {
            // note: needs std::launder as of C++17
            reinterpret_cast<T*>(&data[pos])->~T();
        }
    }
};

template<std::size_t MaxApps>
class tMinExCoordinator
{
    public:
        ...

        void addApp(iMinExApplication* app, const char name[]) 
        {
            Applications.emplace_back(app, name);
        }

        ...

        void runApps()
        {
            for (auto& app : Applications)
            {
                // Frequency check
                // ...
                app.execute();
            }
        }

    private:
        static_vector<tMinExApplication, MaxApps> Applications;
};

I have though of smart pointers, but am unsure if they use heap memory or not.

By default, they rely on new and delete, and thus dynamic memory. Though, you can supply them with pointers to stack memory, if you also supply them with custom deleters that won't free that memory.

Upvotes: 2

Related Questions