Help making a set of C++ Classes

I am beyond rusty at Class Magic but remember some parts and I have a C based task and status method for debouncing to turn into a library.

But it's not simple or easy because I want a base class with derived classes for the many different ways buttons get wired up.

I think that:

The base class needs virtual functions .update() and .read() where read may have multiple args... I once knew how to do that but now there's a hole. The base class lets all instances use a base pointer and those functions.

From the base class derive classes for button, button array, button matrix, etc. Single button .read() has no arg but array needs read( index ) and matrix needs row and col. Same for .update().

Other added features have been suggested with code to catch taps and press durations... IMO those should be in derived classes of their own since they add RAM needed per pin.

The no-class demo, but see below:

// button2026 by GoForSmoke @ Arduino.cc Forum
// Free for use, Oct 23/2026 by GFS. Compiled on Arduino IDE 1.8.19.
// rev 0.001, added direct port pin read. Beginner digitalRead code left as comments. 

/*  Button Debounce Example

  --- for this example connect a button between pin 7 and GND
  --- or stick a jumper in pin 7 and while holding the board steady
  --- tap the free end onto the grounded USB port box to press.
  --- Press and hold the Button to toggle led13 blinking.

  Each button has a history byte that holds the last 8 reads with 256 possible
  states but only 6 of them being significant. Pin mode is INPUT_PULLUP.
  
      detect stable static button
  0 is button held down.
  0xFF is button left up.
  
      detect transition after pre-change stable period
  0x80 is button change from up to down and 7 stable down reads.
  0x7F is button change from down to up and 7 stable up reads.
  
      detect transition then post-change stable period
  0xFE is button change from up to down after 7 stable up reads.
  1 is button change from down to up after 7 stable down reads.
  
  everything else is to be ignored as bounce. Most apps might use 0x80.
  Note that 4 bit history works with good buttons...
  if (( history & 15 ) == 8 ) { down to up after 3 stable reads };
*/

extern volatile unsigned long timer0_millis; // might be faster than millis()

// button vars
byte buttonPin = 7;
byte buttonHistory;
byte lastMillis, nowMillis;

// added sketch task, on-off blinker vars
byte ledState, ledPin = 13; // use byte for small values, int cost 2 bytes
word startBlink, waitBlink; // 16 bit millis is good to time 65.535 seconds

void buttonTask()   // this is the history keeper, 1 read every 1024 micros.
{ 
  nowMillis = ( timer0_millis & 255 );
  if ( lastMillis != nowMillis )
  {
    buttonHistory <<= 1; // moves current reads up and bit 7 is lost, bit 0 = 0

//    beginner friendly half-fast pin read for Uno R3
//    buttonHistory += digitalRead( buttonPin ); // read history streams through buttonHistory

//    alternate faster non-beginner bit logic used pin read for Uno R3
//    PIND & 0x80; // PIND is pins 0 to 7, pin 7 is bit 7
    buttonHistory += PIND & 0x80 ? 1 : 0; // read history streams through buttonHistory
    lastMillis = nowMillis;
  }
}

void OnOffBlinker() // only blinks if there's a wait time, can be switched on/off
{
  if ( waitBlink > 0 ) // this is the on/off switch
  {
    if (( timer0_millis & 0xFFFF ) - startBlink >= waitBlink ) // difference in time by subtracting start from end
    {
      ledState = !ledState;  // ! is NOT: not_0/true becomes 0/false else 0 becomes 1.
      digitalWrite( ledPin, ledState ); // the led changes state.
      startBlink += waitBlink; // next blink starts when it should, where diff > wait.
    }
  }
  else if ( ledState > 0 ) // waitBlink == 0 turns blinking off
  {
    digitalWrite( ledPin, ledState = LOW ); //  make sure the led is OFF
  } // yes, you can set a variable during calculation in C, the write here is LOW.
}

void setup()
{
  Serial.begin( 115200 );
  for ( byte i = 0; i < 66; i++ )  Serial.println();
  Serial.println( F( "\n  Button Debounce Example, free by GoForSmoke\n" ));
  Serial.println( F( "\n-- for this example connect a button between pin 7 and GND" ));
  Serial.println( F( "--- or stick a jumper in pin 7 and while holding the board steady" ));
  Serial.println( F( "--- tap the free end onto the grounded USB port box to press." ));
  Serial.println( F( "--- events reports followed by micros() to show pre from post detect.\n\n" ));

  pinMode( buttonPin, INPUT_PULLUP );
  pinMode( ledPin, OUTPUT );

  buttonHistory = 255;
};


void loop()
{
  buttonTask();
  /*
      detect stable static button
  0 is button held down.
  0xFF is button left up.
  
      detect transition after pre-change stable period
  0x80 is button change from up to down and 7 stable down reads.
  0x7F is button change from down to up and 7 stable up reads.
  
      detect transition then post-change stable period
  0xFE is button change from up to down after 7 stable up reads.
  1 is button change from down to up after 7 stable down reads.
  
  everything else is to be ignored as bounce. Most apps might use 0x80.

   */

  switch ( buttonHistory ) 
  {
    case 0x80 : // pin is HIGH in bit 7 then LOW for 7 reads, up to down detected
      buttonHistory = 0; // change detected, make it into no detect now
      Serial.print( F( "post stable press detected     " ));
      Serial.print( millis());
      if ( waitBlink == 0 ) // toggle action tied to button press
      {
        waitBlink = 500; // makes the blinking start
        startBlink = millis(); // gets the low 16 bits
        Serial.println( F( "     blink" ));
      }
      else
      {
        waitBlink = 0; // makes the blinking stop
        Serial.println( F( "     stop" ));
      }
      break;
    case 0x7F : // pin is LOW in bit 7 then HIGH for 7 reads, down to up detected
      buttonHistory = 0xFF; // change detected, make it into no detect now
      Serial.print( F( "post stable release detected   " ));
      Serial.println( millis());
      Serial.println();
      break;

    case 0xFE : // pin is HIGH for 7 reads then LOW, up to down detected
      buttonHistory = 0xFC; // change detected, make it into no detect now
      Serial.print( F( "pre stable press detected     " ));
      Serial.println( millis());
      break;
    case 1 : // pin is LOW for 7 reads then HIGH, down to up detected
      buttonHistory = 0x3; // change detected, make it into no detect now
      Serial.print( F( "pre stable release detected   " ));
      Serial.println( millis());
      break;
  }

  OnOffBlinker();
}

This is the single pin debounce update task.

// button2026 by GoForSmoke @ Arduino.cc Forum
// Free for use, Oct 23/2026 by GFS. Compiled on Arduino IDE 1.8.19.

extern volatile unsigned long timer0_millis; // might be faster than millis()

void buttonTask()   // this is the history keeper, 1 read every 1024 micros.
{ 
  nowMillis = ( timer0_millis & 255 );
  if ( lastMillis != nowMillis )
  {
    buttonHistory <<= 1; // moves current reads up and bit 7 is lost, bit 0 = 0

//    beginner friendly half-fast pin read for Uno R3
    buttonHistory += digitalRead( buttonPin ); // read history streams through buttonHistory

//    alternate faster non-beginner bit logic used pin read for Uno R3
//    PIND & 0x80; // PIND is pins 0 to 7, pin 7 is bit 7
//    buttonHistory += PIND & 0x80 ? 1 : 0; // read history streams through buttonHistory

    lastMillis = nowMillis;
  }
}

as for .read() and maybe .isPress(), etc ---- I only have ideas. As above, when a transition -is- detected the history needs to get moved forward (assuming no state change) 1 step just to keep another read from triggering before the next history update. Arduino process may check that pin >50 times between updates.

Am I anywhere near on track or just must be on crack?

Would the number of possible argument choices be fixed (eg. .read() can have 1, 2 or 3 arguments) or would the number of argument choices be arbitrary? If the latter, then a template would likely come into play.

The problem is that C++ requires an overriding virtual function to have a compatible signature withthe base-class function. The arguments can't vary in the way you've described.

You can overload functions in C++, but overloading is not the same as overriding ➜ A ButtonArray::read(byte index) doesn't override ButtonBase::read().

what do you expect read() to return for matrix ? for example ? or would you ping each button in turn inside the array and the matrix (in which case you know the index and you don't need any index at all - you talk to the underlying button instance)

I recall being able to make pointers to functions with variable arg lists but not HOW.

I expect that might let derived .read(args) supply the code so for matrix it'd be .read( row, col ) that would return status history and bump it up 1 step.

So maybe I can't have a run-everything base class or even want a base class.

I think that you know this better than I do so what-you-say?

[edit]

 or would you ping each button in turn inside the array and the matrix 

And there's the crux because how I read a matrix using pin state manipulations and there's so many ways to arrange buttons I am feeling that a set of classes may not have a common base at all.

You might consider using delegates instead of having a strict class hierarchy with virtual methods. Might even mix the two: base class delegates for single, array, matrix -- which have the virtual methods.

That is beyond what I used in the 90's. A series of kidney operations left me with lots of clots after/during 2000 and since then I lost a lot of my C++.

I know enough to want common elements in a base class and then use inheritance, etc, to code differences.

OK lets start simple..

In HUMAN what are you looking for in in your common class?

Variadic function pointers won't save it even if read() were declared
virtual byte read(...)
you'd still have to manually unpack va_list in every override with zero compile-time checking that you passed the right types/count, and calling it through a Button* wouldn't magically know how many args to push.

It's a tool for things like printf, not for giving virtual functions flexible signatures.

Ditching the base class isn't the right move either: You could just give read() a uniform signature across the whole hierarchy, even though different subclasses use the args differently.

virtual byte read(byte a = 0, byte b = 0) = 0;

SingleButton would ignore both arguments
ButtonArray uses a as the index
ButtonMatrix uses a as row and b as col.

It's kinda ugly-ish, but it keeps update() and read() both callable uniformly through a Button*, which is the whole point of having a base class in the first place.

base would be

class Button {
  public:
    virtual void update() = 0;
    virtual byte read(byte a = 0, byte b = 0) = 0;
};

and then you could have subclasses as

class SingleButton : public Button {
  public:
    SingleButton(byte p) : pin(p) {}
    void update() override;
    byte read(byte a = 0, byte b = 0) override;   // a, b unused

  private:
    byte pin;
    byte history;
};

class ButtonArray : public Button {
  public:
    ButtonArray(const byte* p, byte n)
      : count(n), pins(new byte[n]), history(new byte[n]) {
      for (byte i = 0; i < n; i++) pins[i] = p[i];
    }
    ~ButtonArray() { delete[] pins; delete[] history; }
    void update() override;
    byte read(byte a = 0, byte b = 0) override;   // a = index, b unused

  private:
    byte count;
    byte* pins;
    byte* history;
};

class ButtonMatrix : public Button {
  public:
    ButtonMatrix(const byte* r, byte nr, const byte* c, byte nc)
      : numRows(nr), numCols(nc),
        rows(new byte[nr]), cols(new byte[nc]),
        history(new byte[nr * nc]) {
      for (byte i = 0; i < nr; i++) rows[i] = r[i];
      for (byte i = 0; i < nc; i++) cols[i] = c[i];
    }
    ~ButtonMatrix() { delete[] rows; delete[] cols; delete[] history; }
    void update() override;
    byte read(byte a = 0, byte b = 0) override;   // a = row, b = col

  private:
    byte numRows, numCols;
    byte* rows;
    byte* cols;
    byte* history;
};

and you could create an array of mixed buttons

constexpr byte buttonPin = 10;
constexpr byte arrPins[]  = { 2, 3, 4 };
constexpr byte rowPins[]  = { 5, 6 };
constexpr byte colPins[]  = { 7, 8, 9 };

Button* buttons[] = {
  new SingleButton(buttonPin),
  new ButtonArray(arrPins, sizeof arrPins),
  new ButtonMatrix(rowPins, sizeof rowPins, colPins, sizeof colPins)
};

polling for update would be simple

for (auto* b : buttons)  b->update();

but you would still need to know the type to call read() with the right number of parameters.

Update gets run in loop(). A multi-button update should on timed interval update 1 button per loop() to keep from blocking longer than necessary and updating ALL in 1 loop() is not necessary.

The update interval for big arrays and matrices may need to be smaller than for single buttons do it when millis changes. When I first made debounce-history it was over a button box and the update was every 500 micros to get the whole box updated in 1 or 2 ms. So for some classes, interval is a initializer.

It's the read that the for-next might get used for the way I see it ...

but

you guys let me know if I'm clunking?

I want update independent of read, to be a task in loop() that always runs but doesn't always update unless of course it has to just to keep up with all the buttons.

I have this weird idea that monitoring inputs all the time will take all of that collecting and deciding off process and output tasks. It should change how the app uses buttons.

I'm also beginning to see collecting pin history at coarser scales to see how patterns of button use (coded knocks to Morse taps?) might be reduced to single integers to avoid a lot of structured code.

Would it help if I post older array and matrix examples?

Some of that uses AVR port manipulation, the matrix code especially which is going to need change to work on ESP and other chips. As long as INPUT and OUTPUT can both be HIGH or LOW the method should work, I kinda like how AVR port registers work.

This thread could get me to change my examples to include libraries. I just dunno, would that be better?

PS OTOH I can update older array and matrix examples and members that can school me in Class can make libraries that I can't.

seems like a lot of discussion on "how" to do it.

what about it just returning the index of a button that is pressed? or that a button has been pressed for some period of time or at least period of time?

What the example shows keeps pin status history and checks that different ways as examples of what can be detected in that simple and fast way that only uses 2 bytes of RAM per pin. To keep track of more like time... I posted about that twice now besides how it should be in Classes for those needs. Fast and feature-itis do not co-exist. Let's have both as choices.

By including the slim update function and vars of my example in the void loop() of other non-blocking code, functions in that loop() can detect press and release transitions two different ways and steady up or down as well. That does include 1 step of housekeeping if a transition is detected.

I wouldn't want to lose that in a Class BUT .isPress() and like that methods can be added. Classes can be derived from a Base as J-M-L described or a set of Classes may result.

I can see a Class collecting pin state history over intervals greater than debounce and longer than 8 bits just to see what button press patterns could be detected so simply. History bits encode relative time and data, all kinds of logic that writes itself don't it?

You could use stub functions so the child classes don't have to override ALL of the read() variants:

class Button {
  public:
    virtual byte read() {
      return 0;
    }
    virtual byte read(byte a = 0) {
      return 0;
    }
    virtual byte read(byte a = 0, byte b = 0) {
      return 0;
    }
};

class SingleButton : public Button {
  public:
    byte read() override;
};

class ButtonArray : public Button {
  public:
    byte read(byte a = 0) override;
};

class ButtonMatrix : public Button  {
  public:
    byte read(byte a = 0, byte b = 0) override;

};

Interesting! Looks clean.

My guess is that data members would go in derived classes and the base would have a virtual update() function. As long as read(...) returns a pin history then fine so far.

What to do about pattern detection, the base may get even more virtual functions. But that's far down the road, let's burn that bridge before we get to it!

Update: I'm making a start, already surfing for syntax. It took a while to remember the word constructor to look up those initializer thingies.

I named my base class ButtonBase so that Button is for one's.

To not depend on AVR timer0_millis change, history update periods will be timed in 16-bit micros() because millis() is +/-1.

uint16_t updatePeriodMicros;

uint16_t updatePeriodStart;

and perhaps more virtual functions in the base class;

virtual bool isPreStablePress();

virtual bool isPostStablePress(byte a);

virtual bool isPreStableRelease(byte a, byte b);

virtual bool isPostStableRelease();

virtual bool isPostStableRelease(byte a);

virtual bool isPostStableRelease(byte a, byte b);

virtual bool isDown();

virtual bool isDown(byte a);

virtual bool isDown(byte a, byte b);

virtual bool isUp();

virtual bool isUp(byte a);

virtual bool isUp(byte a, byte b);

and all of those need to advance history to ensure one detect per event.

Interesting idea

I think there is likely an issue with the default values as read() with no parameters can match the three signatures - probably a challenge if there is a call through a pointer .

I need to think a bit more about it but something is puzzling me.

Ok after my morning coffee and more thoughts I think what is annoying me is that defaults are what make read() and read(1) ambiguous, because several overloads then match the same call.

There are two issues here.

The first one is about the defaults, and removing them is enough. With plain overloads, each call resolves to exactly one signature, and each derived class overrides only the one it implements. On a concrete object, no extra mechanism is needed, because when a derived class declares any member function called read, that declaration hides every base class function with the same name, regardless of signature. The compiler stops searching in the derived class once it finds the name, so the base overloads are never considered, even when their parameters would match the call. That way a ButtonMatrix refuses read() and read(1) at compile time, and I am forced to give it two parameters.

So this would work - almost your code, just no default.

class Button {
  public:
    virtual byte read() { return 0; }
    virtual byte read(byte a) { return 0; }
    virtual byte read(byte a, byte b) { return 0; }
};

class SingleButton : public Button {
  public:
    byte read() override;
};

class ButtonArray : public Button {
  public:
    byte read(byte a) override;
};

class ButtonMatrix : public Button {
  public:
    byte read(byte a, byte b) override;
};

The second issue is that this protection disappears when calling through a pointer or reference to Button, if you want some sort of polymorphic use, which is what this construct suggests. The compiler looks up the name read in the scope of the static type, which is Button, where all three overloads are declared, so nothing is hidden. A call like b->read(1) on a ButtonMatrix therefore compiles and silently returns 0 from the base stub instead of failing to compile.

I need more coffee for that one :slight_smile: - may be it’s not an issue and can be called a feature, it’s up to the developer to do the right thing…

More thoughts on the second point

If we were on a more modern platform, we could leverage the fact that no call should ever reach the implementation in the base class, since reaching it means the actual type does not support that signature. The stubs could then raise a real error instead of returning 0, for example with an exception:

class Button {
  public:
    virtual ~Button() {}
    virtual const char* name() const = 0;
    virtual byte read()           { throw std::logic_error(std::string(name()) + ": unsupported read()"); }
    virtual byte read(byte)       { throw std::logic_error(std::string(name()) + ": unsupported read(byte)"); }
    virtual byte read(byte, byte) { throw std::logic_error(std::string(name()) + ": unsupported read(byte, byte)"); }
};

But on our small MCU we cannot throw in that way, so a poor man's implementation could be to flag the error on the serial monitor and carry on:

class ButtonBase {
  public:
    virtual const char* name() const = 0;
    virtual byte read()           { return fail("read()"); }
    virtual byte read(byte)       { return fail("read(byte)"); }
    virtual byte read(byte, byte) { return fail("read(byte, byte)"); }
  protected:
    byte fail(const char* signature) {
      Serial.print(name());
      Serial.print(": unsupported ");
      Serial.println(signature);
      return 0;
    }
};

Or to simply crash at runtime after logging the error, so that the developer knows what happened and fixes it:

  protected:
    byte fail(const char* signature) {
      Serial.print(name());
      Serial.print(": unsupported ");
      Serial.println(signature);
      Serial.flush();      // make sure the message is fully sent before stopping
      while (true) {yield();}      // halt here
    }

Anyway may be TMI…

Or, just return an invalid state for the button. Since a button only has two valid states (maybe three if you count "unstable"), that leaves you with many possible byte values to return as "Invalid Call to read()".

Good point

I am beginning to think that of base class not serving enough purpose to be worth the trouble.. or code.