Struggling passing multi-dimensional arrays to class [solved]

I found this wonderful FAQ about passing multi-dimensional arrays to functions, but I’m struggling getting this to work with a class.

I can get it to work fine, in one arduino sketch without a class

const int totalOctaves = 10;
const int totalTriggers = 6; //max 128 (hard coded into the class. Also, only 128 possible MIDI notes.

const int midiNotes[][totalTriggers] = {
  {20,21,22,23,24,25}, // octave 0
  {30,31,32,33,34,35}, // octave 1 
  {40,41,42,43,44,45}, // octave 2 
  {50,51,52,53,54,55}, // octave 3
  {60,61,62,63,64,65}, // octave 4
  {70,71,72,73,74,75}, // octave 5
  {80,81,82,83,84,85}, // octave 6
  {90,91,92,93,94,95}, // octave 7
  {100,101,102,103,104,105}, // octave 8
  {110,111,112,113,114,115}  // octave 9
};

int _notes[128];

template< size_t N >
  void setManyNotes1D( int (&ptr)[N] ){
 
    for( int idx = 0 ; idx < N ; ++idx ){
      _notes[idx]=ptr[idx];
    }
}

void printNotes(int t){
  Serial.print("_notes[");
  
  for(int o = 0; o < t; o++){
    Serial.print(_notes[o]);
    Serial.print(", ");
  }
  
  Serial.println("]"); 
}

void setManyNotes2D(const int (*ptr)[totalTriggers], int slot, int totalOct){
  for(int o = 0; o < totalOct; o++){
    _notes[o]=ptr[o][slot];
  }
}

template< typename T, size_t N, size_t X >
  void setManyNotes2Dv2( T (&ptr)[N][X], int slot ){
    for( int idx = 0 ; idx < N ; ++idx ){
      _notes[idx]=ptr[idx][slot];
    }
}

void setup() {
  Serial.begin(9600);
  Serial.println("setup begin");
  
  setManyNotes2Dv2(midiNotes,2);
  printNotes(totalOctaves);
  
}

void loop() {
}

The following code narrows down the problem. I’m trying to use the ‘template’ syntax to allow the 2D array to be any size.

//MIDItrigger.h

#ifndef MIDItrigger_h
#define MIDItrigger_h

#include "Arduino.h"

class MIDItrigger{
  private:
    int _notes[128]; //Prep an array to hold the info for them all. Max octaves allowed 128. How to set this dynamically with the constructor?  
  public:
    MIDItrigger();
    void setManyNotes( T (&ptr)[N][X], int slot);
};

#endif
//MIDItrigger.cpp

#include "Arduino.h"
#include "MIDItrigger.h"

MIDItrigger::MIDItrigger() 
{  //default values
   int _notes[1]; 
}

//use a template so this works with all numbers of triggers and octaves
template< typename T, size_t N, size_t X >
  void MIDItrigger::setManyNotes( T (&ptr)[N][X], int slot )
  {//stores many notes
    for( int idx = 0 ; idx < N ; ++idx ){
      _notes[idx]=ptr[idx][slot];
    }
}
//arduino sketch classTestsm.ino

#include<MIDItrigger.h>

//midi note mapping
const int totalOctaves = 10;
const int totalTriggers = 6; //max 128 (hard coded into the class. Also, only 128 possible MIDI notes.

const int midiNotes[][totalTriggers] = {
  {20,21,22,23,24,25}, // octave 0
  {30,31,32,33,34,35}, // octave 1 
  {40,41,42,43,44,45}, // octave 2 
  {50,51,52,53,54,55}, // octave 3
  {60,61,62,63,64,65}, // octave 4
  {70,71,72,73,74,75}, // octave 5
  {80,81,82,83,84,85}, // octave 6
  {90,91,92,93,94,95}, // octave 7
  {100,101,102,103,104,105}, // octave 8
  {110,111,112,113,114,115}  // octave 9
};

//setup triggers
MIDItrigger t0; 

void setup() {  
  t0.setManyNotes(midiNotes,0); //set the notes
}

void loop() {
}
// errors

In file included from classTestsm.ino:1:
/Users/chrisevans/Documents/Arduino/libraries/MIDItrigger/MIDItrigger.h:11: error: 'T' has not been declared
/Users/chrisevans/Documents/Arduino/libraries/MIDItrigger/MIDItrigger.h:11: error: 'N' was not declared in this scope
/Users/chrisevans/Documents/Arduino/libraries/MIDItrigger/MIDItrigger.h:11: error: 'X' was not declared in this scope
classTestsm.ino: In function 'void setup()':
classTestsm:24: error: no matching function for call to 'MIDItrigger::setManyNotes(const int [10][6], int)'
/Users/chrisevans/Documents/Arduino/libraries/MIDItrigger/MIDItrigger.h:11: note: candidates are: void MIDItrigger::setManyNotes(int)

Can anyone point me in the right direction with the syntax?

Thanks.

Not sure why you’re confused… The compiler is pointing you at this line:

void setManyNotes( T (&ptr)[N], int slot);

which is referencing T, N, and X, which you don’t define prior to referencing them here.

If you’re going to use a template, you have to define the template BEFORE you attempt to create an instance of a function from that template.

Regards,
Ray L.

I could see the line that was the problem, but I didn’t understand what it meant. I kept trying to define a variable T to be used by the function.

But I figured out that I needed to move the tempalte function to be defined in the header file, not the .cpp file.

//MIDItrigger.h

#ifndef MIDItrigger_h
#define MIDItrigger_h

#include "Arduino.h"

class MIDItrigger{
  private:
    int _notes[128]; //Prep an array to hold the info for them all. Max octaves allowed 128. How to set this dynamically with the constructor?  
  public:
    MIDItrigger();
    
    //use a template so this works with all numbers of triggers and octaves
	template< typename T, size_t N, size_t X >
		void setManyNotes( T (&ptr)[N][X], int slot )
  			{//stores many notes
    			for( int idx = 0 ; idx < N ; ++idx ){
      				_notes[idx]=ptr[idx][slot];
    			}
		}
};

#endif
//MIDItrigger.cpp

#include "Arduino.h"
#include "MIDItrigger.h"

MIDItrigger::MIDItrigger() { }

This works. Thanks for the response.

Thanks for the good review (It’s my article :slight_smile: )

Just to fill in the blanks…

Why it does not work in the .cpp:

The compiler needs to be able to see the template to fill in the blanks when you attempt to use it. Just like Ray mentioned (defined before use, another .cpp is not going to see the definition, just the declaration).

This is fundamentally why people usually keep the template function or class entirely in the header.

You can put it in a .cpp file, you however need to instantiate whichever versions of the template you’ll need.
For your example, you’d place something like the code below at the end of your .cpp to explicitly provide an instantiation which client code could use:

template<> void MIDItrigger::setManyNotes< int, 4, 5 >( int (&ptr)[4][5], int slot );

However this isn’t very useful, and you might as well have written a flat function with no template at all.

Its usefulness would be found when you need to restrict what can be used with the template. An example could be something like a floating point function/class which uses a template type to define its working data. By keeping the code in a .cpp, you could provide instantiation only for floating point types (float, double, long double). Then if a user tries to provide an int as the template parameter, the code will not compile.

const int midiNotes[][totalTriggers] = {
  {20,21,22,23,24,25}, // octave 0
  {30,31,32,33,34,35}, // octave 1 
  {40,41,42,43,44,45}, // octave 2 
  {50,51,52,53,54,55}, // octave 3
  {60,61,62,63,64,65}, // octave 4
  {70,71,72,73,74,75}, // octave 5
  {80,81,82,83,84,85}, // octave 6
  {90,91,92,93,94,95}, // octave 7
  {100,101,102,103,104,105}, // octave 8
  {110,111,112,113,114,115}  // octave 9
};

int? To store values in the range 0 to 255?

PaulS you caught me. When I start coding I'm often lazy about variable types and then tighten things up later. That's probably bad form. I should change most of those ints to bytes. Thanks for calling it out.

pYro_65 thanks for the clarification. It all makes sense now.

That's probably bad form.

It is. Understanding the limitations of the hardware you are working with, and not abusing them, will let you get a lot farther before you run into problems.

This means minimizing SRAM usage all the time.