Search code examples
c++embeddedavrcomposition

C++ composition without using the heap in embedded systems


I'm fairly new to C++, this is also my first post on here. I'm trying to use C++ in an embedded systems project so I can take the OOP approach. I'm using the AVR crosspack toolchain (AVR G++ compiler)

My problem is this:

From what i've read, the heap should not be used for dynamic memory allocation in embedded systems. In any case, there is no implementation for "new" in AVR G++ anyway. I'm using composition, starting with a USART driver (lets call it a service), and a logger (singleton pattern, and also a service).

It's my understanding that services should have their dependancies passed in on instantiation using constructor parameters, however when I try to compose the objects needed in this way I get the following error:

Main/main.cpp: In function 'int main()':
Main/main.cpp:21:13: error: request for member 'log' in 'logSystem', which is of non-class type 'LogSystem(Usart)'
   21 |   logSystem.log("Hello");
      |             ^~~
make: *** [Main/main.o] Error 1

My sense is that my syntax for passing in an object as a constructor parameter is wrong, but I'm not sure what it should be as all the examples i can find use the "new" keyword in the constructor definition to create the object on the free store. Can anyone help?

The Code:

In "usart.h":

#include <avr/io.h>
#include <util/setbaud.h>

class Usart
{
public:
  // Constructor and destructor
  Usart();
  ~Usart();

  // Initialisation routine
  static void const init(void);

  // Utility function to transmit a string
  static void const print(const char myString[]);
};

In "logger.h":

#include "usart.h"

class LogSystem
{
public:

  LogSystem(Usart usart);
  ~LogSystem();

  Usart usart;

  static void const log(char *msg);

};

In "logger.cpp"

#include "logger.h"

LogSystem::LogSystem(Usart usart)
{
  Usart usart;
  usart.init();
}

LogSystem::~LogSystem()
{
}

LogSystem::log(char *msg)
{
  usart.print(msg);
}

In "main.cpp":

#include "logger.h"


int main()
{
    LogSystem logSystem(Usart usart);

    while(1)
    {
        logSystem.log("Hello");
    }

    return 0;
}

Solution

  • [...] the heap should not be used for dynamic memory allocation in embedded systems.

    It depends. I'm currently in an embedded project with maximum safety-related requirements, and we use new, but not delete. So we have a heap, but don't allocate "dynamically", because all allocated objects are kept during the runtime.

    In any case, there is no implementation for "new" in AVR G++ anyway.

    Is this true? I never checked... It might be necessary to provide a heap before being able to use new.

    It's my understanding that services should have their dependancies passed in on instantiation using constructor parameters, [...]

    This is a good idea. ;-) It helps unit-testing.

    For your syntactical and design problems: This is how I would write your sources.

    "usart.h":

    All methods are non-static to have access to member variables.

    The const attribute on a return type is doing nothing. Did you mean to declare the method constant? Then const belongs after the parameter list. However, this attribute might be wrong if such a method changes any member variable.

    #include <avr/io.h>
    #include <util/setbaud.h>
    
    class Usart
    {
    public:
      Usart();
      ~Usart();
    
      void init(void);
    
      void print(const char myString[]);
    };
    

    "logger.h":

    Just give and store a reference to the USART to avoid a copy.

    #include "usart.h"
    
    class LogSystem
    {
    public:
      LogSystem(Usart& usart);
      ~LogSystem();
    
      void log(const char *msg);
    
    private:
      Usart& _usart;
    };
    

    "logger.cpp"

    The member variable _usart is directly initialized in the constructor, before any statement is executed.

    #include "logger.h"
    
    LogSystem::LogSystem(Usart& usart) : _usart(usart)
    {
      _usart.init();
    }
    
    LogSystem::~LogSystem()
    {
    }
    
    void LogSystem::log(const char *msg)
    {
      _usart.print(msg);
    }
    

    "main.cpp":

    Provide the USART object on the stack, as the logger.

    #include "logger.h"
    
    int main()
    {
        Usart usart;
        LogSystem logSystem(usart);
    
        while(1)
        {
            logSystem.log("Hello");
        }
    
        return 0;
    }
    

    The singleton design pattern is deprecated since it was invented, because it is so hard to test. Simply use just one object or a limiting object factory.