How to specify the size of a class array ?

Ouch! You're right. For the #define approach to work, either the whole class must be in a single .h file (as in the template case) or the #define'd constant must be in a separate .h file that has to be included by both the sketch and the class header file.

Templates are your best bet, none of the side effects of #defines, a template type is bound to its parameters so consider a class:

template< int V > struct Foo{ static const int MyNum = V; };

This struct is global, you are guaranteed that "Foo< 5 >::MyNum" is always the same value, no matter what compilation unit. This may not be the clearest example, if MyNum was equal to something else other than V it would still have the same guarantee.

You can also put the class definition in a cpp file, I'll describe below:

lib.h

template< typename T >
  class Foo{
    public:
      Foo( T t_In );
      T Value( void );
    private:
      T t_Data;
};

lib.cpp

  #include "lib.h"

  template< typename T >
    Foo< T >::Foo( T t_In ) : t_Data( t_In ) {}

  template< typename T >
    Foo< T >::Value( void ){ return this->t_Data; }

This set up will lead to a linker error. This error is because the compilation unit cannot see the definition of the template. To fix this you can do two things: instantiate types where the definition is available, or use the the definition rather than the declaration.

If you know what range/discrete values/types users may select for the template you can instantiate them directly in the cpp ( lib.cpp ) file

//Add instantiations below definitions in cpp file.
template class Foo< bool >;
template class Foo< char >;
template class Foo< long >;

In your sketch you can include the header file and happily use the template with bool, char, and long however int will cause an error.
This is a basic way of stopping programmers from misusing the library with bad inputs.

The other way is simply include the cpp file instead of the header, solving the missing definition error.

Here is my template array class you can give a try:

template< typename _Type, uint16_t _Len > 
  struct MPLArray{
    enum{ 
      Length = _Len,
      Size = sizeof( _Type ) * _Len,
    };
  _Type& operator[]( uint16_t u_Index ) { return this->t_Data[ u_Index ]; }
  operator _Type*() { return this->t_Data; }
  operator const _Type*() const { return this->t_Data; }
  
  _Type t_Data[ Length ];
};	

void loop(){
  MPLArray< bool, 5 > bb = { true, true, true, false, false };
  bool *bp = bb;
  Serial.print( bb[0] );
  Serial.print( *( bp + 3 ) );
  Serial.print( bb.Length );
  Serial.print( bb.Size );
}

You could also create as a variable in a class, or derive a class from it to inherit the functionality.

PaulS:

What about growing the array ? Is it safe ?
That would happen when the user would call addBoard() more times than the available slots in the array.

Until all available memory was used, it is safe.

I should probably RTFM about this :slight_smile: but in the meantime, do you mean realloc() is "safe", i.e. doesn't have problems like free() ?

tuxduino:

PaulS:

What about growing the array ? Is it safe ?
That would happen when the user would call addBoard() more times than the available slots in the array.

Until all available memory was used, it is safe.

I should probably RTFM about this :slight_smile: but in the meantime, do you mean realloc() is "safe", i.e. doesn't have problems like free() ?

Essentially, with the library bug, it's okay to allocate memory as long as you don't expect it back.

realloc calls free if the new chunk size is smaller

@pYro_65

Thank you for you detailed response and for the code.

Out of curiosity, why a struct instead of a class, and why make everything public ? Just asking...

I modified it a bit, included addItem.

MPLArray (modified)

#ifndef _MPL_ARRAY_H
#define _MPL_ARRAY_H

#include <stddef.h>
#include <stdint.h>

template< typename _Type, uint16_t _MaxCount > 
class MPLArray{
public:
    MPLArray() {
        _count = 0;
    };
    
    void addItem(_Type& newItem, bool* itemAdded = NULL, uint16_t* itemIdx = NULL) {
        if (!isFull()) {
            t_Data[_count] = newItem;
            
            if (itemAdded != NULL) {
                *itemAdded = true;
            }
            
            if (itemIdx!= NULL) {
                *itemIdx = _count;
            }
            
            _count++;
        }
        else {
            *itemAdded = false;
        }
    };
    
    bool isFull() const {
        return _count < _MaxCount;
    };
    
    uint16_t count() const {
        return _count;
    };
    
    uint16_t maxCount() const {
       return _MaxCount;
    }; 
    
    uint16_t size() const {
        return sizeof( _Type ) * _MaxCount;
    };
    
    _Type& operator[]( uint16_t u_Index ) {
        return this->t_Data[ u_Index ];
    };
    
    operator _Type*() {
        return this->t_Data;
    };
    
    operator const _Type*() const {
        return this->t_Data;
    };
    
private:
    _Type t_Data[ _MaxCount ];
    uint16_t _count;
};

#endif

Boards.h

#ifndef _BOARDS_H
#define _BOARDS_H

#include "MPLArray.h"

template <int MAX_BOARD_CNT>
class Boards : public MPLArray<int, MAX_BOARD_CNT> {
public:
    Boards() {};

    void addBoard(int boardId) {
         MPLArray<int, MAX_BOARD_CNT>::addItem(boardId);
    };
    
    // other Boards-specific code...
};

#endif

Main sketch:

#include "Boards.h"

Boards<100> boards;

int freeRam () {
    extern int __heap_start, *__brkval; 
    int v; 
    return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
};

void setup() {
    Serial.begin(115200);
    Serial.println(boards.maxCount());
    Serial.println(boards.count());
    boards.addBoard(5);
    boards.addBoard(10);
    Serial.println(boards.count());
    Serial.println(freeRam());
}

void loop() {
}

Count = 100

Code size: 2704
Freeram: 1578

WizenedEE:
Essentially, with the library bug, it's okay to allocate memory as long as you don't expect it back.

realloc calls free if the new chunk size is smaller

That would fit my use case, as I don't expect the array to shrink, so probably a malloc() + expand as needed with realloc could be ok.

(I'm thinking about a delItem() method, but that would just "invalidate" an entry or shift everything down 1 position and decrement _count, without shrinking the array).

It's true that using dynamic memory allocation is usually best avoided in embedded systems (see Dynamic Memory Allocation in Critical Embedded Systems | David Crocker's Verification Blog for why). However, allocating dynamic memory using malloc or new in the initialization phase (i.e. in stuff called from setup()) is OK if you never release it.

The template solution is also OK. One issue with using templates in embedded systems is that the code gets replicated each time the template is instantiated, leading to code bloat; but in this case you will only even instantiate the class once, so that doesn't apply.

Have you considered using a linked list instead of an array? That way you don't need to know in advance how many boards there will be. The disadvantage is that you need a little more RAM to store the links.

dc42:
It's true that using dynamic memory allocation is usually best avoided in embedded systems (see Dynamic Memory Allocation in Critical Embedded Systems | David Crocker's Verification Blog for why). However, allocating dynamic memory using malloc or new in the initialization phase (i.e. in stuff called from setup()) is OK if you never release it.

Thank you for that very interesting and informative article.

dc42:
The template solution is also OK. One issue with using templates in embedded systems is that the code gets replicated each time the template is instantiated, leading to code bloat; but in this case you will only even instantiate the class once, so that doesn't apply.

You mean if I instantiate two classes with the same template parameters I have two copies of the same "generated" code ? That's interesting. Going to google a bit about that...

But yes, the idea is to have just one instance of that class per sketch.

dc42:
Have you considered using a linked list instead of an array? That way you don't need to know in advance how many boards there will be. The disadvantage is that you need a little more RAM to store the links.

That's an interesting option. For the use case I'm thinking about I think a statically sized array is a better option though.

  1. Do nothing. Pretend G++ does implement automatic instantiation management. Code written for the Borland model works fine, but each translation unit contains instances of each of the templates it uses. In a large program, this can lead to an unacceptable amount of code duplication.

:astonished:

You mean if I instantiate two classes with the same template parameters I have two copies of the same "generated" code ? That's interesting. Going to google a bit about that...

Nope, standard C++ requires types to be unique, therefore only one definition of code can ever be used to describe a type.

tuxduino:
Template Instantiation (Using the GNU Compiler Collection (GCC))

  1. Do nothing. Pretend G++ does implement automatic instantiation management. Code written for the Borland model works fine, but each translation unit contains instances of each of the templates it uses. In a large program, this can lead to an unacceptable amount of code duplication.

:astonished:

Does not apply here, it is for systems that can have page faults, these models help prevent them. ( page faults can be detrimental to games and such ):

When used with GNU ld version 2.8 or later on an ELF system such as GNU/Linux or Solaris 2, or on Microsoft Windows, G++ supports the Borland model. On other systems, G++ implements neither automatic model.

Only ill formed code will have bloat, there is no reason why common code cannot be placed in a base class and inherited into a non-common template.

The instantiations I posted previously will not increase the size of the program unless you actually use the instantiation, they just allow the compiler to generate object files for the required template types:

template class Foo< bool >;
template class Foo< char >;
template class Foo< long >;

Using initial memory allocation is only of benefit if you require the memory to be un-initialised. Even then a global array/variable is still just as effective.

Out of curiosity, why a struct instead of a class, and why make everything public ? Just asking...

POD/Aggregate types cannot have private or protected members, it makes code easier to read, a one line struct is fairly readable, a class may not be.

C++ is your friend :).

tuxduino:
I want the user to be able to specify a size. At compile time.

I don't see your objection to using malloc then (or new). If it is specified at compile time, you won't change it, therefore you wan't call free() therefore you won't have any problems.

The number of items can be in the constructor (or classname.begin). Then the class allocates exactly the amount of memory it needs, and hangs onto it. No problems.

pYro_65:
Nope, standard C++ requires types to be unique, therefore only one definition of code can ever be used to describe a type.

A templated class instantiated twice with different template parameters gives rise to two distinct types.

dc42:

pYro_65:
Nope, standard C++ requires types to be unique, therefore only one definition of code can ever be used to describe a type.

A templated class instantiated twice with different template parameters gives rise to two distinct types.

Yes, that is correct. My response was in regard to multiple instantiations with the same parameters. i.e. the same type.

pYro_65:

dc42:

pYro_65:
Nope, standard C++ requires types to be unique, therefore only one definition of code can ever be used to describe a type.

A templated class instantiated twice with different template parameters gives rise to two distinct types.

Yes, that is correct. My response was in regard to multiple instantiations with the same parameters. i.e. the same type.

My doubt is about code duplication when using a template class with the same parameters in two compilation units. E. g. I have to classes, each written with the usual header/cpp file separation. They both #include MPLArray.h and they both have a data member which is an implementation if the template class with the same parameters, say MPLArray<int, 10>.
The sketch #includes both classes header files, instantiates and uses one (global) object of each class.
Will the final executable contain only one instance of the MPLArrary<int, 10> code, or will there be code duplication ?

tuxduino:
My doubt is about code duplication when using a template class with the same parameters in two compilation units. E. g. I have to classes, each written with the usual header/cpp file separation. They both #include MPLArray.h and they both have a data member which is an implementation if the template class with the same parameters, say MPLArray<int, 10>.
The sketch #includes both classes header files, instantiates and uses one (global) object of each class.
Will the final executable contain only one instance of the MPLArrary<int, 10> code, or will there be code duplication ?

What should happen is that the compiler will instantiate the template once for each file, then the linker will spot that they are the same and only include one of them in the final code. To test this, if you change the second parameter for one of the instantiations, you should see the code size jump up.

tuxduino:
Will the final executable contain only one instance of the MPLArrary<int, 10> code, or will there be code duplication ?

There will only ever be one instance of the executable code if that's what you mean. Each compilation unit gets its own copy of the template methods in the object code but they are marked with the .weak attribute which causes the linker to discard duplicates.

Didn't know about that weak attribute. Goggled a bit about it, and found it in Arduino hardware serial implementation. Interesting... Thanks.

The compiler implicitly marking functions as weak is different to explicitly using a function modifier, may not be portable either.

The serial case usage is not used on a template, and is there to stop multiple definition errors and allow the user to implement the body of serialEvent in their own code.

The hardwareSerial.cpp file is compiled first so without having a weak definition that the linker can throw away, it would have an 'unresolved external. When your sketch is compiled with a new function body, the linker will select it and discard all the others.

Thanks pYro, I wouldn't have been able to write it down as clearly as you did, but your description confirms what I understood about the "weak" mechanics by reading gcc page about it and man nm.