2

I have a class with 12 instances of a button, to create a keypad( meaning that this is the only case I use 12 instances). But this class can be just a button array of any other number between 1-12. I'm looking for a way to minimize class overhead of 12 instances, when I most of the time use much less.

'.h' file:

class buttonArrayTFT
{
public:
  buttonArrayTFT(XPT2046_Touchscreen &_ts = ts, Adafruit_ILI9341 &_tft = tft);
  void create_array(uint8_t R, uint8_t C, char *but_txt[]);
  uint8_t checkPress(TS_Point &p);

public:
  
protected:
  ButtonTFT _button0;
  ButtonTFT *_buttons[12] = {&_button0, &_button1, &_button2, &_button3,
                             &_button4, &_button5, &_button6, &_button7,
                             &_button8, &_button9, &_button10, &_button11};
private:
  ButtonTFT _button1;
  ButtonTFT _button2;
  ButtonTFT _button3;
  ButtonTFT _button4;
  ButtonTFT _button5;
  ButtonTFT _button6;
  ButtonTFT _button7;
  ButtonTFT _button8;
  ButtonTFT _button9;
  ButtonTFT _button10;
  ButtonTFT _button11;
  
private:
  uint8_t _num_items = 0;
};
2
5

As pointed out in a comment on your original question, a template is likely the best solution.

Templates allow you to write highly flexible code. I won't explain why; that's something that goes a little beyond the actual question. However, the following will show that you can create a template that means you can create a buttonArrayTFT class with N number of buttonTFT elements, so long as N is known at compile time. The link given as a comment talks describes various aspects of templates but does not necessarily explain how to string it all together.

From the section Non-type parameters for templates, you will see that you can specify an integer as a parameter for creating N elements of an array member variable.

But what about the type parameter, T? You can simply omit it. You don't actually have to create a template for a type.

In your .h file:

template <uint8_t ARRAY_SIZE>
class buttonArrayTFT
{
public:
// One thing to remember about templates: you have to pack the definition
// into the header file with the declaration to avoid all manner of headaches.

    buttonArrayTFT(XPT2046_Touchscreen &_ts = ts, Adafruit_ILI9341 &_tft = tft)
    {
    // Perhaps this is a good opportunity to check that the specified
    // size of the array is sensible, i.e. not zero and no more than 12.
        if ((ARRAY_SIZE == 0) || (ARRAY_SIZE > 12))
        {
            // decide how you want to handle this problem
        }
            // Add the rest of your constructor here
    }

    void create_array(uint8_t R, uint8_t C, char *but_txt[]);
    uint8_t checkPress(TS_Point &p);
        
    // This little piece of magic allows you to access buttonTFT 'x' of the
    // private _buttonArray as if a buttonArrayTFT class was an array
    buttonTFT &operator[](uint8_t index)
    {
    // Check that 'index' is a valid element in the array
        if (index > _num_items)
        {
            // decide how you want to handle this problem
        }
        return _buttonArray[index];
    }
private: 
    buttonTFT _buttonArray [ARRAY_SIZE];
    // I recommend making this constant
    const uint8_t _num_items = ARRAY_SIZE;
};

I shall leave it up to you to sort out your privates, publics and protecteds, and I have left out ButtonTFT _button0; to keep you on your toes.

You'll also notice that the array is private, as before, but there is now no array-of-pointers-to-buttons. This is because there is no need; the magic buttonTFT &operator[](int index) allows you to access array elements directly.

In your .ino file, or as appropriate, you use the following code to create and then wield the class:

buttonArrayTFT<4> myFourButtonArray(/* constructor arguments as required*/);
buttonArrayTFT<12> myTwelveButtonArray(/* constructor arguments as required*/);

This gives you two instances of the class, with the respective number of buttonTFT elements! You can now access specific buttons using the following code:

myFourButtonArray[0];    // Returns a reference to the first buttonTFT
myTwelveButtonArray[10]; // Returns a reference to the eleventh buttonTFT
myFourButtonArray[n]     // Returns a reference to the 'n+1'th buttonTFT

On the assumption that the buttonTFT class has some kind of 'read' function that returns a TRUE or FALSE for the status of the button, you can write:

if (myFourButtonArray[0].read())
{
    // executed if the button is ON
}
else
{
    // executed if the button is OFF
}

A NOTE ON CONST-NESS:

The overloading of operator[], while wonderfully magic, is open to abuse. However, this is no different from your original code. The array of buttonTFT elements is private but there is public access to all the buttons almost as if the array was not private. In the case of your original code, one could write:

myButtonArray._buttons[0] = myButtonArray._buttons[1];

Now _buttons[0] points to the same buttonTFT as _buttons[1].

Ouch. Now myButtonArray._buttons[0]->read() is literally the same thing as myButtonArray._buttons[1]->read().

That said, my code does not make it a great deal better; the equivalent code:

myFourButtonArray[0] = myFourButtonArray[1];

Simply copies the right hand side over to the left hand side. You still have two distinct buttonTFT instances, with probably no memory leaks, but I am guessing that buttonTFT class may contain a reference to an Input pin, so the _button[0] pin will now be whatever _button[1] pin is because the member variable that stores the pin reference will be overwritten.

To solve this weakness, simply add the const qualifier to the beginning of the operator[] overload:

const buttonTFT &operator[](uint8_t index)
{ // };

Now the buttonTFT element cannot be altered. This of course brings in a new headache that all functions that rely on myArray[n] also need to be constant. It is up to you to determine how constant correctness should be interpreted and whether it is worth the hassle.

10
  • 2
    theArray was likely meant to refer to _buttonArray member (rather than parameter).
    – timemage
    Oct 16 '21 at 12:41
  • 1
    I wrote the code example with my own member names and missed one of them when I was changing them to match yours. Answer has been edited. Oct 16 '21 at 13:19
  • 1
    @Guy.D (1) Yes, you can write whatever you want in a .h file, providing it is genuine C++ code of course. When it comes to templates, you cannot easily split the definition and the declaration into separate files because of the way the the compilation works. Google has plenty of articles on the topic. (2) My apologies, that was left over from when I wrote the example and couldn't remember what you had called your members. Oct 16 '21 at 13:24
  • 1
    @Guy.D (3) The '&' actually belongs to the buttonTFT type specifier, but convention dictates that it is written this way. The symbol means that the function returns a reference. The concept of Operators might confuse matters, so have a read about them in this article (4) I cannot show you how the .cpp file looks as I do not know what is in it! I can tell you that any code relating to the templated class belongs in the .h file, everything else goes in the .cpp file. Oct 16 '21 at 13:32
  • 1
    @Guy.D It will return whatever type you declare the operator function to return. In this case, you want operator[] to return a reference to a buttonTFT, not a reference to a buttonArrayTFT because that doesn't make any sense. You also don't want it to return a pointer because _buttonArray is an array of buttonTFT, not buttonTFT*. Also, as I explained in my answer, *(buttonTFT[x]) = (*buttonTFT[y]) will leave you with the left hand pointer pointing to the right hand pointer, but only for the duration of that line of code, so in theory nothing would actually happen. Oct 18 '21 at 15:18
4

You can use a template class to provide compile time parameters to your class to tune it. This is most often used in the context of providing a type for the class to operate on. However it's not limited to that. It's also often used to provide a storage size or other similar constant to the class.

They kind of work like preprocessor macros. You specify a token in the class and then give that token a value when you instantiate an object. That token can then be used all over the place in your class.

In your case you can provide the number of elements in your array of buttons as a template variable, for example (note: untested):

template <int N> class buttonArrayTFT {
    protected:
        ButtonTFT _buttons[N];
    public:
        void doSomething();
};

template <int N> void buttonArrayTFT::doSomething() {
    for (int i = 0; i < N; i++) {
        _buttons[i].doSomethingOnAButton();
    }
}

Of course you have to treat your objects a little differently since you can't make an array of pointers to predefined objects any more. Instead just a simple array of objects should be enough.

You then instantiate your class passing the number of elements you want:

// Array of 12 buttons
buttonArrayTFT<12> myButtons;
// and do something
myButtons.doSomething();
5
  • Does 'ButtonTFT _buttons[N]' replace the entire definition? in my code I use a pointer array to N instances, so it seems lacking something (or my understanding)
    – guyd
    Oct 16 '21 at 4:11
  • I wrote the class not inside a library, and all was well. BUT, when I write it inside my lib file, I get errors. How should it be noted that way ?
    – guyd
    Oct 18 '21 at 10:27
  • 1
    @Guy.D What errors are you getting?
    – Majenko
    Oct 18 '21 at 11:16
  • Please see my answer below
    – guyd
    Oct 18 '21 at 11:39
  • @Guy.D Because the template is created at compile time you can't create a separate TU with it in - it effectively has to be 100% inline. It will only work in the header file.
    – Majenko
    Oct 18 '21 at 11:58
2

There are more options to solve your question.


To use a template as recommended in the other answers is a fancy option, but if you use a template multiple times, it creates a real class for every template value and you will see it on used flash size.


The best option in my opinion is to provide the array to the class over a constructor. Create a global array of buttons and hand it over to buttonArrayTFT object over a constructor.

const uint8_t BUTTON_COUNT = 12;
ButtonTFT buttons[BUTTON_COUNT];
ButtonArrayTFT buttonArrayTFT(buttons, BUTTON_COUNT);

The ButtonArrayTFT class (simplified) would be like:

class ButtonArrayTFT {
private:
  ButtonTFT *buttons;
  uint8_t count;
public:
  ButtonArrayTFT(ButtonTFT *_buttons, uint8_t _count) {
    buttons = _buttons;
    count = _count;
  }

Other option is to use dynamic allocation on heap. If you would create a ButtonArrayTFT object only once at start and never delete it it would not cause heap fragmentation.

class ButtonArrayTFT {
private:
  ButtonTFT *buttons;
  uint8_t count;
public:
  ButtonArrayTFT(uint8_t _count) {
    count = _count;
    buttons = new ButtonTFT[count];
  }
3
  • I'm trying to "digest" all new things in one question (polymorphism,inline,&operator,template ...ahhhhhhh). I'll try understand the way you suggested in a while :) TNX!
    – guyd
    Oct 18 '21 at 17:56
  • The global approach is a good option if you don't mind having direct access to the buttons from anywhere within the code. Hiding them within a class reminds 'future me', and anyone else who inherits the code, that they're hidden for a reason. No idea what the reason is but it prevents unwanted fiddling. The dynamic allocation would benefit from a memory tracker; a static variable that increases by count * sizeof(button) in the constructor, plus maybe a static_assert, will prevent hitting a memory wall. Oct 20 '21 at 21:45
  • And +1 for the line about creating one complete class for each distinct number of buttons. I hadn't considered that if the same technique is used on numerous GUI elements the program space could soon evaporate. Oct 20 '21 at 21:52
0

I want to share my final updated library (thanks to answers by Majenko&Charlie), perhaps it'll help others.

.h file

template <uint8_t N>
class buttonArray_TFT
{
public:
  int8_t dx = 5;         /* define spacing between buttons */
  int8_t dy = 5;         /* define spacing between buttons */
  uint8_t scale_f = 100; /* change the cale of array. 100% take entire screen */
  uint8_t shift_y = 255; /* Shifts in y director*/
  uint8_t shift_x = 255; /* Shifts in x director*/
  int shrink_shift = 0;  /* shrink array in pixels, and shifts up/ down (+/-) */

  uint8_t &a = butarray[0].a;
  uint8_t &b = butarray[0].b;
  uint8_t &txt_size = butarray[0].txt_size;
  uint16_t &txt_color = butarray[0].txt_color;
  uint16_t &border_color = butarray[0].border_color;
  uint16_t &face_color = butarray[0].face_color;
  bool &roundRect = butarray[0].roundRect;

protected:
  ButtonTFT butarray[N];

public:
  buttonArray_TFT(XPT2046_Touchscreen &_ts = ts, Adafruit_ILI9341 &_tft = tft);
  void create_array(uint8_t R, uint8_t C, char *but_txt[]);
  uint8_t checkPress(TS_Point &p);
};

and more in : .h file :

template <uint8_t N>
buttonArray_TFT<N>::buttonArray_TFT(XPT2046_Touchscreen &_ts, Adafruit_ILI9341 &_tft)
{
  for (int i = 0; i < N; i++)
  {
    butarray[i].TS[0] = &ts;
    butarray[i].TFT[0] = &tft;
  }
}

template <uint8_t N>
void buttonArray_TFT<N>::create_array(uint8_t R, uint8_t C, char *but_txt[])
{
\\ Code 
}

template <uint8_t N>
uint8_t buttonArray_TFT<N>::checkPress(TS_Point &p)
{
  for (uint8_t i = 0; i < N; i++)
  {
    if (butarray[i].checkPress(p))
    {
      return i;
    }
  }
  return 99;
}
2
  • 1
    You cannot split the code into two files. EVERYTHING for the buttonTFT class must be in the .h file, as both Majenko and I have said. I know it goes against all convention that you have otherwise been taught but templates are the exception to the rule. Oct 18 '21 at 12:17
  • 1
    @CharlieHanson - Thanks. I'll correct my answer. Appreciate explanaiton about operator
    – guyd
    Oct 18 '21 at 12:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.