Returning a reference to an array element - Sensible?

I have a large array of 'Foo's and I want to be able to externally point to specific elements in the array, almost as if each was a variable name. I don't want to have a huge list of instantiated Foo's as I need them in an array anyway to be able to iterate over them all later.

I feel like, elsewhere in my code, writing foo* myFirstFoo = fooArr[133] is a risky and potentially error-prone venture. What if I miscounted and meant 134? I could add comments for each element of the array but what if they get cut/pasted in to a new order? Or again I've mistyped or miscounted the comments, or if I add in new elements halfway through. Forgetting that hidden somewhere, was the fooArr[133] reference could be a damning bug!

So, in order to avoid this, I've taken the following approach and would basically like to throw it to the wolves to see if it survives. What are the pitfalls, naiveties and oversights in the following? (Or, is it a valid way to solve the issue?)

#define ARRSIZE(x) sizeof(x) / sizeof(x[0])

class Foo {
  public:
    char const *name;
    int ID; 
  Foo(char const *name, int ID)
  : name(name), ID(ID) { }
};

class Bar {
  public:
    Foo* myFirstFoo;
    Foo* mySecondFoo;
    Foo* myThirdFoo;

  void displayFoos () {
    Serial.println(myFirstFoo->name);
    Serial.println(mySecondFoo->name);
    Serial.println(myThirdFoo->name);
  }
};

Foo fooArr[10] = {
  { "Foo 0", 1000 },
  { "Foo 1", 1001 },
  { "Foo 2", 1002 },
  { "Foo 3", 1003 },
  { "Foo 4", 1004 },
  { "Foo 5", 1005 },
  { "Foo 6", 1006 },
  { "Foo 7", 1007 },
  { "Foo 8", 1008 },
  { "Foo 9", 1009 },
};

Foo* fooFinder(char const *findName, int findID) {
  for (byte i = 0; i < ARRSIZE(fooArr); i++) {
    if (!strcmp(fooArr[i].name, findName) && fooArr[i].ID == findID) {
      return &fooArr[i];
    }
  }
}

void setup() {
  Serial.begin(9600);
  
  Bar bar;
  bar.myFirstFoo = fooFinder("Foo 7", 1007);
  bar.mySecondFoo = fooFinder("Foo 3", 1003);
  bar.myThirdFoo = fooFinder("Foo 5", 1005);

  bar.displayFoos();
}

void loop() { }

The fooFinder() function would be run a collection of times once at 'start-up' as a kind of helper tool to set things up.

Simply referring to a Foo by ID alone seems okay but maybe (user)error-prone, and searching JUST based on 'name' could be a problem if names are doubled (as they may be).

But, simple copy/pasting the correct elements name/ID into the relevant fooFinder() call seems to be sensible.

Is this an approach that's used at all by 'people who actually know what they're doing'? :grin:

you need to use strcmp() to compare cStrings. here you would be comparing if the pointers are the same

1 Like

Oops! Noted and edited.

that could work and be "dynamic".

If all is statically done, I could see going the opposite way: Define the Foos you want to have direct access on "manually" and add them to the array, something like this - fully untested.

class Foo {
  public:
    char const *name;
    int ID;
    Foo(char const *name, int ID): name(name), ID(ID) {}
};

class Bar {
  public:
    Foo* myFirstFoo;
    Foo* mySecondFoo;
    Foo* myThirdFoo;

    void displayFoos () {
      Serial.println(myFirstFoo->name);
      Serial.println(mySecondFoo->name);
      Serial.println(myThirdFoo->name);
    }
};

Foo foo1("Foo 7", 1007);
Foo foo2("Foo 3", 1003);
Foo foo3("Foo 5", 1005);

Foo fooArr[10] = {
  { "Foo 0", 1000 },
  { "Foo 1", 1001 },
  { "Foo 2", 1002 },
  foo2,
  { "Foo 4", 1004 },
  foo3,
  { "Foo 6", 1006 },
  foo1,
  { "Foo 8", 1008 },
  { "Foo 9", 1009 },
};

void setup() {
  Serial.begin(9600);

  Bar bar;
  bar.myFirstFoo = &foo1;
  bar.mySecondFoo = &foo2;
  bar.myThirdFoo = &foo3;

  bar.displayFoos();
}

void loop() {}

aren't arrays of structs a conventional way of processing tables of data?

are their special elements that need unique references? why

(Tested and it does work).
Interesting! Strangely, I've never considered a 'mixed' approach to arrays in that way before, so that could be useful. If all of the elements are accessed 'somewhere' (in various different places), then it would end up similar to creating all the foos globally and having an additional array of pointers, would it not?

sure -goriness aside, there are multiple ways to skin that cat (hate that expression).

define the instances one by one lets you access them with a variable name

adding them to an array lets you deal with all the instances in a for loop easily.

Note that if you could let the class maintain that array of instances for you automatically by having a class array or vector variable in which you maintain all the instances (do that in the constructor and destructor).

The 'real' use case in this example is a big list of (yikes!) 752 'parameters'. Almost all are accessed from inside a menu, but some are 'pulled out' to be accessed via various button actions.

So I may only have 14 buttons, but when you account for short and long press actions across several modes I'm 'pulling out' maybe 28 parameters (short and long for each button) per mode and maybe 3 modes.

So enough for it to get confusing to reference them directly as fooArr[473]. I'm still learning (aren't we all?) and every time I learn a new concept I find better (hopefully!) ways of writing the same code. This is my pleasure project and also my sandbox for coding concepts. I've started over from scratch maybe 8 times and I'm refactoring parts constantly. So the potential for references to various parameters to get lost is huge (hence the helper function!)

what are you saying? that you want to associate specific array elements to each button?

are these hardcoded references (e.g. myFirstFoo = fooArr[133]) or are they dynamically determined at run time based on a search?

The plot thickens! Do you feel like you're playing Sherlock sometimes when answering these? I feel like I'm stumbling into an 'XY Problem' trap :smiley:

Yes. Essentially. Strap in... This is why I presented an example isolating my issue/question instead.

The intent wasn't to get into the details on the project, mainly just the way of accessing array elements by reference. Anyway here's an overview.

It's a midi foot-controller for a guitar amp. The amp has a masssssive list of parameters that can be accessed. On the amp itself, only a handful can be adjusted with knobs and buttons. If you hook up the PC and their software, you get the full deal and full access to tweak things. If you don't have the computer, or want to change any of these settings while playing guitar you're out of luck.

My controller gives you the full (at your feet) access away from the PC, via an LCD, menu and a whole bunch of footswitches and encoders. The project (in a simpler form) works a treat!

The parameters, in part, use an index (they also hold an address from the amp side for that parameter). For example, say there are 10 types of 'delay' choices, one of the parameters is PRM_DLY_TYPE holding the index for the current type of delay. This can be accessed inside the menu with the encoders along with the 700+ other params, but also in the right mode on the board, I can press a footswitch to cycle the current delay types.

Hence needing the full array.

When the amp spits out the index data for a certain parameter address, say, the amp is telling me which delay type is currently set, I need to iterate over the full parameter list on the board side, to update the correct index.

I.e:

void parseIncoming(unsigned long incomingAddress, uint16_t incomingData) {
  for (uint16_t i = 0; i < NUM_PARAMS; i++) {
    if (allParams[i].address == incomingAddress) {
        allParams[i].index = incomingData;
        break;
    }
  }
}

So I need the full list in an array. It's quite messy and easy to mess up declaring 700+ instances, then having them all referenced in an array too. So it's easier to just have an array of Parameters from the start. But... Then I need to pull out references for the menus/buttons to access specifics.

Going back a real example might be:

Parameter allParams[NUM_PARAMS] = {
      { "Boost Switch", 0x60000010,  0,   1,  1 },
      { "Boost Colour", 0x60000639,  0,   2,  1 },
      { "Boost Type",   0x60000011,  0,  22,  1 },
      { "Boost Level",  0x60000657, -1, 100,  1 }
      // etc etc for 700+ more
};

Then my finder helper (used on startup for setting up the buttons and menus):

    Parameter* findParam(char const * findTitle, uint32_t findAddress) {
      for (uint16_t i = 0; i < NUM_PARAMS; i++) {
        if (!strcmp(allParams[i]->title, findTitle) && allParams[i]->sysex == findAddress)
          return &allParams[i];
      }
      while (1) {
        Serial.print("ERR! findParam() ");
        Serial.println(findAddress);
        delay(1000);
      }
      return nullptr; // Shouldn't reach this, but to be safe.
    }

I guess in one sense they are kind of hardcoded, in that I intend on setting them once only, and them not to change. But to hardcode them, I would need to refer to them in the code via allParams[XYZ] which is the bug-prone approach I was looking to avoid in the original post.

That said, because they are resolved at runtime I guess they have the option of changing. I haven't declared them const yet, but I could without affecting current behaviour.

I have a hunch that doesn't "put" foo2, foo3, foo1 in the array. It puts copies of foo2, foo3, foo1 in the array. Since Foo doesn't have an explicit Copy Constructor, they will be trivially copied (i.e. byte for byte). That's OK in this case since Foo is such a simple class. But something to be aware of.

you are 100% right.

keeping an array of pointer will do the trick

class Foo {
  public:
    char const *name;
    int ID;
    Foo(char const *name, int ID): name(name), ID(ID) {}
    Foo(const Foo& f) : name(f.name) , ID(f.ID) {  // copy constructor
      Serial.print(f.name); 
      Serial.println(" is being copied");
    }
};

class Bar {
  public:
    Foo* myFirstFoo;
    Foo* mySecondFoo;
    Foo* myThirdFoo;

    void displayFoos () {
      Serial.println(myFirstFoo->name);
      Serial.println(mySecondFoo->name);
      Serial.println(myThirdFoo->name);
    }
};

Foo foo1("Foo 7", 1007);
Foo foo2("Foo 3", 1003);
Foo foo3("Foo 5", 1005);

Foo* fooArr[] = {
  new Foo("Foo 0", 1000),
  new Foo("Foo 1", 1001),
  new Foo("Foo 2", 1002),
  &foo2,
  new Foo("Foo 4", 1004),
  &foo3,
  new Foo("Foo 6", 1006),
  &foo1,
  new Foo("Foo 8", 1008),
  new Foo("Foo 9", 1009),
};

void setup() {
  Serial.begin(115200);

  Bar bar;
  bar.myFirstFoo = &foo1;
  bar.mySecondFoo = &foo2;
  bar.myThirdFoo = &foo3;

  bar.displayFoos();
  Serial.println(fooArr[0]->name);
}

void loop() {}

putting the array creation in the setup() after the Serial.begin() should help see that the copy constructor is not called

When you said

if you could let the class maintain that array of instances for you automatically

Did you mean like this?

#define NUM_FOOS 10

class Foo {
  public:
    static Foo* list[NUM_FOOS];
    static uint16_t fooCount;
    char const *name;
    int ID;
    Foo(char const *name, int ID): name(name), ID(ID) {
      list[fooCount++] = this;
    }

    static void printAllFoos() {
      for (uint16_t i = 0; i < fooCount; i++) {
        Serial.println(list[i]->name);
      }
    }
}; uint16_t Foo::fooCount; Foo* Foo::list[NUM_FOOS];

Foo foo0("Foo 0", 1000);
Foo foo1("Foo 1", 1001);
Foo foo2("Foo 2", 1002);
Foo foo3("Foo 3", 1003);
Foo foo4("Foo 4", 1004);
Foo foo5("Foo 5", 1005);
Foo foo6("Foo 6", 1006);
Foo foo7("Foo 7", 1007);
Foo foo8("Foo 8", 1008);
Foo foo9("Foo 9", 1009);

class Bar {
  public:
    Foo* myFirstFoo;
    Foo* mySecondFoo;
    Foo* myThirdFoo;

    void displayFoos () {
      Serial.println(myFirstFoo->name);
      Serial.println(mySecondFoo->name);
      Serial.println(myThirdFoo->name);
    }
};

void setup() {
  Serial.begin(115200);
  Bar bar;
  bar.myFirstFoo = &foo3;
  bar.mySecondFoo = &foo5;
  bar.myThirdFoo = &foo7;
  bar.displayFoos();

  // Foo::printAllFoos();
}

void loop() {}

It does allow me to have individual instances but also keep an array of pointers that can be iterated over.

Another consideration of having an array of Foos and a 'fooFinder' vs a list of foos and a foo* array, was memory considerations. Especially considering how many foos/parameters in my real world version.

yes, kindof.

you need to implement as well the destructor to remove the pointer from the array and possibly the copy constructor

What would be the purpose of removing the pointer in this case? As the Foos will be constructed on start-up, and maintained for the life of the program (I don't intend on them being scoped out)

if everything is static then I would not bother with that capability then.

how do you get to 752 with that?

Because they are all in the menu, but some are also in the buttons

Let me try to recap my understanding / guesses

  • your amp offers 700+ parameters and looking at your example
Parameter allParams[NUM_PARAMS] = {
      { "Boost Switch", 0x60000010,  0,   1,  1 },
      { "Boost Colour", 0x60000639,  0,   2,  1 },
      { "Boost Type",   0x60000011,  0,  22,  1 },
      { "Boost Level",  0x60000657, -1, 100,  1 }
      // etc etc for 700+ more
};

it seems the structure holds a label, an amp side address and what I'd guess is min, max and current value (?)

Then what you want to do is statically assign one parameter to a user action (short or long press) on a given button (15 of them). May I guess that short press selects that settings and long press allows for modification and one encoder is used to modify the current value?

You don't even need to bother with pointers. Just have the fooFinder() function return the array index of the element found. Run that function for the "special cases" at the beginning and store the returned indices in uint16_t (or size_t) variables.