Using an array in a class

In my quest to understand classes better I am trying to create a class for some functions I have for displaying things on an OLED. The code I have does things like splitting the display into lines at the top for non-scrolling text with the remaining text as scrolling. What exactly my code does isn't so important as that it needs an array to hold the text that will be sent to the display. I thought using an array in a class would be simple...

Here are some of my attempts with the corresponding error messages:

class OLED_helper {
    private:
        bool requestDisplayUpdate;
        uint8_t displayNumRows;         // Total number of rows of text on the display
        uint8_t displayNumCols;         // Total number of columns on the display
        uint8_t displayNumStaticRows;   // Rows at the top reserved for static non-scrolling text, remaining rows used for scrolling text
        char displayBuffer[displayNumRows][displayNumCols];
        
    public:
        OLED_helper(const uint8_t rows, const uint8_t cols, const uint8_t staticRows) : displayNumRows(rows), displayNumCols(cols), displayNumStaticRows(staticRows) {}
};

invalid use of non-static data member 'OLED_helper::displayNumRows'

I understand why this fails, because the compiler wants to know the size of the array at compile time, which it doesn't. I hope this example illustrates what I'm trying to do though.

I've gone through various attempts at modifying the above, my latest attempt is this:

class OLED_helper {
    private:
        bool requestDisplayUpdate;
        uint8_t displayNumRows;         // Total number of rows of text on the display
        uint8_t displayNumCols;         // Total number of columns on the display
        uint8_t displayNumStaticRows;   // Rows at the top reserved for static non-scrolling text, remaining rows used for scrolling text
        char * displayBuffer;
    
    public:
        OLED_helper(const uint8_t rows, const uint8_t cols, uint8_t staticRows) {
            displayNumRows = rows;
            displayNumCols = cols;
            displayNumStaticRows = staticRows;
            displayBuffer = new char[rows][cols];
        }
};

array size in new-expression must be constant

It seems to me that the array size is constant, but the compiler says otherwise and in my long running battle with the compiler the compiler has yet to lose.

Questions, suggestions, comments and advice most welcome please.

I have come across some solutions for this, but they are horrible! Please don't tell me there is no better answer than these:

https://stackoverflow.com/questions/936687/how-do-i-declare-a-2d-array-in-c-using-new
Reply #356

int** ary = new int[sizeY][sizeX]

should be:

int **ary = new int*[sizeY];
for(int i = 0; i < sizeY; ++i) {
    ary[i] = new int[sizeX];
}

And then clean up would be:

for(int i = 0; i < sizeY; ++i) {
    delete [] ary[i];
}
delete [] ary;

Dietrich Epp pointed out in the comments this is not exactly a light weight solution. An alternative approach would be to use one large block of memory:

int *ary = new int[sizeX*sizeY];

// ary[i][j] is then rewritten as
ary[i*sizeY+j]

a template could be used to define an array when you create the object


template <uint8_t displayNumCols, uint8_t displayNumRows>
class OLED_helper {
    private:
        bool requestDisplayUpdate;
        //uint8_t displayNumRows;         // Total number of rows of text on the display
        //uint8_t displayNumCols;         // Total number of columns on the display
        uint8_t displayNumStaticRows;   // Rows at the top reserved for static non-scrolling text, remaining rows used for scrolling text
        char displayBuffer[displayNumRows][displayNumCols];
        
    public:
        OLED_helper(const uint8_t staticRows) : displayNumStaticRows(staticRows) {}
};

OLED_helper <16, 2> helper(42);

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

Thanks for the suggestion.
I've tried that but it fails with the unhelpful:

exit status 1
Error compiling for board Raspberry Pi Pico W.

I don't intend to post the full error here as it is because this is a small part of of something bigger. If needed I'll create an MRE. Here is the code as I have it, perhaps you can spot a silly mistake:

template <uint8_t displayNumCols, uint8_t displayNumRows>
class OLED_helper {
    private:
        bool requestDisplayUpdate;
        //uint8_t displayNumRows;         // Total number of rows of text on the display
        //uint8_t displayNumCols;         // Total number of columns on the display
        uint8_t displayNumStaticRows;   // Rows at the top reserved for static non-scrolling text, remaining rows used for scrolling text
        //char displayBuffer[16][16];
        //char displayBuffer[128][128];
        //char * displayBuffer;
        
        char displayBuffer[displayNumRows][displayNumCols];
        
    public:
        OLED_helper(const uint8_t staticRows) : displayNumStaticRows(staticRows) {}
        /*
        OLED_helper(const uint8_t rows, const uint8_t cols, const uint8_t staticRows) {
            displayNumRows = rows;
            displayNumCols = cols;
            displayNumStaticRows = staticRows;
            displayBuffer = new char[rows][cols];
        }
        */
        //OLED_helper(const uint8_t rows, const uint8_t cols, const uint8_t staticRows) : displayNumRows(rows), displayNumCols(cols), displayNumStaticRows(staticRows) {}
        //void init();
        //void TextToStaticRows(char text[], uint8_t row);
};
OLED_helper <20, 16> helper(2);

Assuming there is a silly mistake and your code works as I intend it does leave me with another problem, which is I need to preserve the array dimensions for later use by code that uses the array, this is the purpose of the following in my attempts:

        OLED_helper(const uint8_t rows, const uint8_t cols, const uint8_t staticRows) {
            displayNumRows = rows;
            displayNumCols = cols;
            displayNumStaticRows = staticRows;
            displayBuffer = new char[rows][cols];
        }

I don't see how your suggestion allows anything other than creating the array, I also need to set the values of uint8_t displayNumRows; and uint8_t displayNumCols;

Edit:
I've put your suggestion into a new sketch and it compiles (well, I thought it would!), so I must be missing something simple. I'll keep looking.

You don't. Just use the template parameters (displayNumCols & displayNumRows) where you need them.

template <uint8_t displayNumCols, uint8_t displayNumRows>
1 Like

Of course!
Thank you :grinning:

@noiasca ,
I've found my error*, it was because:

OLED_helper <20, 16> helper(2);

Was in the h file, resulting in multiple definitions. I don't understand this as I have guards on my header files. Anyway, moving it to a cpp file fixed it.

*This is a lie, a friend found my error for me :shushing_face:

I can't get that to work, I get the following error:

'template<unsigned char displayNumRows, unsigned char displayNumCols> class OLED_helper' used without template arguments

h file:

template <uint8_t displayNumRows, uint8_t displayNumCols>   // Total number of rows of text on the display, Total number of columns on the display
class OLED_helper {
    private:
        bool requestDisplayUpdate;
        uint8_t displayNumStaticRows;   // Rows at the top reserved for static non-scrolling text, remaining rows used for scrolling text
        char displayBuffer[displayNumRows][displayNumCols];
        
    public:
        OLED_helper(const uint8_t staticRows) : displayNumStaticRows(staticRows) {}
        
        void TextToStaticRows(char text[], uint8_t row);
        
};

C++ file:

OLED_helper <16, 20> helper(2);

void OLED_helper::TextToStaticRows(char text[], uint8_t row) {
    if (row < displayNumStaticRows) {
        snprintf(displayBuffer[row], displayNumCols, "%s", text);
        requestDisplayUpdate = true;
    } else {
        // Error to scrolling text about trying to print to invalid row
    }
}

Put everything in a .hpp or try

template <uint8_t displayNumRows, uint8_t displayNumCols>
void OLED_helper<displayNumRows, displayNumCols>::TextToStaticRows(char text[], uint8_t row) {
  if (row < displayNumStaticRows) {
    snprintf(displayBuffer[row], displayNumCols, "%s", text);
    requestDisplayUpdate = true;
  } else {
    // Error to scrolling text about trying to print to invalid row
  }
}

In C++, when defining a template class member function outside the class body, you must repeat the template declaration and fully qualify the class with its template parameters.

I didn't make templates work with separate .h / .c files. I use them in header only files.

Thank you @J-M-L , that compiles.

Unfortunately when I made an MRE to test working and progress I ran into another problem:

Ino file:

#include "someFile.h"

OLED_helper <16, 20> helper(2);

//template <uint8_t displayNumCols, uint8_t displayNumRows>
void setup() {
  Serial.begin(115200);
  delay(2000);

  helper.testPrint();

}

void loop() {
  // put your main code here, to run repeatedly:

}

someFile.cpp:

#include "someFile.h"

template <uint8_t displayNumRows, uint8_t displayNumCols>
void OLED_helper<displayNumRows, displayNumCols>::testPrint(){
    Serial.print("Cols: ");
    Serial.println(displayNumCols);

    Serial.print("Rows: ");
    Serial.println(displayNumRows);

    Serial.print("Static rows: ");
    Serial.println(displayNumStaticRows);
}

someFile.h:

#ifndef someFile
#define someFile
#include <arduino.h>

template <uint8_t displayNumCols, uint8_t displayNumRows>
class OLED_helper {
    private:
        uint8_t displayNumStaticRows;
        char displayBuffer[displayNumRows][displayNumCols];
        
    public:
        OLED_helper(const uint8_t staticRows) : displayNumStaticRows(staticRows) {}
        void fillArray();
        void testPrint();
};

#endif

Error:

c:/users/perry bebbington/appdata/local/arduino15/packages/rp2040/tools/pqt-gcc/2.1.0-a-d3d2e6b/bin/../lib/gcc/arm-none-eabi/12.3.0/../../../../arm-none-eabi/bin/ld.exe: C:\Users\PERRYB~1\AppData\Local\Temp\arduino_build_474600\sketch\Arrays_in_class.ino.cpp.o: in function `setup':
C:\Users\Perry Bebbington\Documents\Arduino\Forum questions\Array use in a class\Arrays_in_class/Arrays_in_class.ino:15: undefined reference to `_ZN11OLED_helperILh16ELh20EE9testPrintEv'
collect2.exe: error: ld returned 1 exit status
exit status 1
Error compiling for board Raspberry Pi Pico W.

If I remove the call to helper.testPrint(); in setup then it compiles, but obviously that's not much use.

I then tried putting the call to helper.testPrint(); in a new non-class function at the bottom of someFile.cpp, which compiled and worked as expected, like this:

#include "someFile.h"


template <uint8_t displayNumRows, uint8_t displayNumCols>
void OLED_helper<displayNumRows, displayNumCols>::testPrint(){
    Serial.print("Cols: ");
    Serial.println(displayNumCols);

    Serial.print("Rows: ");
    Serial.println(displayNumRows);

    Serial.print("Static rows: ");
    Serial.println(displayNumStaticRows);
}


OLED_helper <16, 20> helper(2);

void test() {
    helper.testPrint();
}

Then an new cpp file with same function as above, which failed to compile:

#include "someFile.h"

OLED_helper <16, 20> helper(2);

void test() {
    helper.testPrint();
}

Error:

C:\Users\PERRYB~1\AppData\Local\Temp\ccf1FlyH.ltrans0.ltrans.o: In function `main':
<artificial>:(.text.startup+0x158): undefined reference to `OLED_helper<(unsigned char)16, (unsigned char)20>::testPrint()'
collect2.exe: error: ld returned 1 exit status
exit status 1
Error compiling for board ATmega4809.

So I seem only to be able to use the class functions (methods?) if I call them from within the same cpp file as they are defined.

No, the problem is you can't use separate .cpp files for function definitions of template classes. So do either:
someFile.h:

#ifndef someFile
#define someFile
#include <Arduino.h>

template <uint8_t displayNumRows, uint8_t displayNumCols >
class OLED_helper {
  private:
    uint8_t displayNumStaticRows;
    char displayBuffer[displayNumRows][displayNumCols];

  public:
    OLED_helper(const uint8_t staticRows) : displayNumStaticRows(staticRows) {}
    void fillArray();
    void testPrint();
};

template <uint8_t displayNumRows, uint8_t displayNumCols>
void OLED_helper<displayNumRows, displayNumCols>::testPrint() {
  Serial.print("Cols: ");
  Serial.println(displayNumCols);

  Serial.print("Rows: ");
  Serial.println(displayNumRows);

  Serial.print("Static rows: ");
  Serial.println(displayNumStaticRows);
}
#endif

OR:
someFile.h:

#ifndef someFile
#define someFile
#include <Arduino.h>

template <uint8_t displayNumRows, uint8_t displayNumCols >
class OLED_helper {
  private:
    uint8_t displayNumStaticRows;
    char displayBuffer[displayNumRows][displayNumCols];

  public:
    OLED_helper(const uint8_t staticRows) : displayNumStaticRows(staticRows) {}
    void fillArray();
    void testPrint();
};

#include "someFile.hpp"

#endif

someFile.hpp:

template <uint8_t displayNumRows, uint8_t displayNumCols>
void OLED_helper<displayNumRows, displayNumCols>::testPrint() {
  Serial.print("Cols: ");
  Serial.println(displayNumCols);

  Serial.print("Rows: ");
  Serial.println(displayNumRows);

  Serial.print("Static rows: ");
  Serial.println(displayNumStaticRows);
}

I only use the .hpp approach

I think I got the two files option to work by having the template implemented in a .tpp file (not sure it was with Arduino) because I had added an #include of the .tpp (Not .cpp as this may confuse the IDE) at the end of the .h

So long story short go for the .hpp as it’s known to work with the IDE (not sure it wil let you play with a .tpp file as the goal is to make sure the IDE does not try to compile it).

If I remember well the issue stems from the fact that the compiler is instantiating the class with the template parameters at the point of variable instantiation and the class definition needs to be known to instantiate the functions hence the need to have them in included in your code directly.

(A templated class generates a new, distinct version of the class for each unique set of template parameters, duplicating both its data members and member functions for each type combination used.)

@gfvalvo , @J-M-L ,

Thank you both, the code compiles and works in my MRE as expected.

Much appreciated.