Renamed 'iterating through a series of objects' - original title: OOP, this pointer questions

Update: Working version at post #82

Update #2: Interesting power cycle anecdote at post #110

Thread contributors: alto777, Delta_G, gfvalvo, Coding_Badly, evanmars, dougp (TO)

I wrote a timer library several years ago.  An example sketch and the library code follow.

Even though the library works I have a few questions.

  • Is the virtual function Reset( ) correctly placed?

  • This occured to me while composing this post: Is a virtual function even needed in this library?

  • Can the this pointer be somehow used to update all timers in one fell swoop as opposed to a series of calls to xxx.update()?  I’ve read some about this (Interrupt in class, ESP32 - #44 by PieterP) but my grasp of the concept is rather tenuous.

  • In class Flasher_tmr is isFlashing() correctly placed or should it be somewhere else?

Sketch I/O is three tactile switches with INPUT_PULLUP connected to 3, 4, A3.  Four common cathode LEDs attached to 6-9. Arduino NANO board.

.cpp and .h files exist as tabs in the IDE.

.ino


// demo program for forum thread:
// https://forum.arduino.cc/t/programming-help-switch-off-relays/1239207
//
// target house switch control:
// A pressed = high house
// B pressed = low house
// A and B pressed = both houses

#include "Multi_Timer.h"

// These timers debounce the switches

OnDelay_tmr buttonDelayA(150);
OnDelay_tmr buttonDelayB(150);

// These timers drive the relays for thrower actuation

OnDelay_tmr LEDTmrA(300);
OnDelay_tmr LEDTmrB(300);
OnDelay_tmr TmrBothPressed(300);

// Timer for example of other type

Flasher_tmr FlashTest(2000, 500); // (preset time, on time)

const byte buttonA = 3;
const byte buttonB = 4;
const byte buttonFlash = A3;
const byte ledA = 6;
const byte ledB = 7;
const byte ledBoth = 8;
const byte ledFlash = 9;

bool bothOn;

enum { idle,
       aPressed,
       bPressed,
       bothPressed } sequenceState;


void setup() {
  Serial.begin(115200);
  pinMode(buttonA, INPUT_PULLUP);
  pinMode(buttonB, INPUT_PULLUP);
  pinMode(buttonFlash, INPUT_PULLUP);

// All LEDs are common cathode

  pinMode(ledA, OUTPUT);
  pinMode(ledB, OUTPUT);
  pinMode(ledBoth, OUTPUT);
  pinMode(ledFlash, OUTPUT);

  Serial.print("started");
}

void loop() {

  buttonDelayA.update();
  buttonDelayB.update();
  LEDTmrA.update();
  LEDTmrB.update();
  TmrBothPressed.update();
  FlashTest.update();

  buttonDelayA.setEnable(!digitalRead(buttonA));
  buttonDelayB.setEnable(!digitalRead(buttonB));

  FlashTest.setEnable(!digitalRead(buttonFlash)); // Start/stop flasher timer
  digitalWrite(ledFlash, FlashTest.isFlashing());
  
  if (buttonDelayA.isRunning() and buttonDelayB.isRunning()) {
    bothOn = true;
    sequenceState = bothPressed;
  }

  if (!bothOn) {
    if (buttonDelayA.isDone()) {
      sequenceState = aPressed;
    } else if (buttonDelayB.isDone()) {
      sequenceState = bPressed;
    }
  }

  // Neither switch pressed
  if (buttonDelayA.isEnabled() == false and buttonDelayB.isEnabled() == false) {
    sequenceState = idle;
    bothOn = false;
  }

  // Turn on LEDs for various states

  LEDTmrA.setEnable(sequenceState == aPressed); // Start/stop timer A
  digitalWrite(ledA, LEDTmrA.isRunning());

  LEDTmrB.setEnable(sequenceState == bPressed); // Start/stop timer B
  digitalWrite(ledB, LEDTmrB.isRunning());

  TmrBothPressed.setEnable(bothOn);
  digitalWrite(ledBoth, TmrBothPressed.isRunning());

  }  // end of loop()


.cpp

// filename: Multi_Timer.cpp  6/18/19

#include "Multi_Timer.h"
#include "Arduino.h"
//
/*
      Multi_timer is the base timer class.
      > Sets done status true upon reaching preset.
      > Produces a positive-going one-scan pulse 'os' upon reaching
      preset value (done status = true) and another one-scan pulse
      when done goes false.
      > responds to a reset command by setting accumulated value to
      zero and resetting _Done and _TimerRunning.
      > This version has preset checking removed.
*/
// one-argument constructor for non-flasher types
Multi_timer::Multi_timer(unsigned long pre) {
  _Preset = pre;
  _Control = false;
}

// two-argument constructor for flasher type
Multi_timer::Multi_timer(unsigned long pre, unsigned long onTime) {
  _Preset = pre;
  _OnTime = onTime;
  _Control = false;
}

//  ===== Access functions for timer status/controls

void Multi_timer::setEnable(bool en) {
  _Enable = en;
}

void Multi_timer::setReset(bool res) {
  _Reset = res;
}

void Multi_timer::setCtrl(bool ctrl) {
  _Control = ctrl;
}

bool Multi_timer::isEnabled() {
  return _Enable;
}

bool Multi_timer::isReset() {
  return _Reset;
}

bool Multi_timer::isDone() {
  return _Done;
}

bool Multi_timer::isRunning() {
  return _TimerRunning;
}

bool Multi_timer::isIntv() {
  return _TimerRunning;
}

bool Multi_timer::isOSRise() {
  return _Done_Rising_OS;
}

bool Multi_timer::isOSFall() {
  return _Done_Falling_OS;
}

// The virtual function 'Reset' enables the individual
// functionality of the various timer types.

bool reset();  // virtual function

//========================================================

//  The 'update' function is the heart of the thing.
//  ----------------------------------------------------
//  Updates timer accumulated value & conditions flags '_Done',
// '_Done_Rising_OS', '_Done_Falling_OS' and '_TimerRunning'.
//
//  Returns boolean status of _Done.  update() must be called
//  periodically in loop to update the flags and accumulated
//  value.
//  ====================================================

bool Multi_timer::update() {
  _CurrentMillis = millis();  // Get system clock ticks
  if (_Enable or _Control) {  // timer is enabled to run
    _Accumulator = _Accumulator + _CurrentMillis - _LastMillis;
    if (_Accumulator >= _Preset) {  // timer done?
      _Accumulator = _Preset;       // Don't let accumulator run away
      _Done = true;
    }
  }
  _LastMillis = _CurrentMillis;

  if (reset()) {  // Call virtual reset function.  Reset timer if
    //              returns true, based on derived class' criteria.
    _Done = false;
    _Accumulator = 0;
    _Control = false;  // ensures reset of latched type
  }
  /*
    ----- Generate a positive going one-shot pulse on _Done false-to-true transition
  */
  _Done_Rising_OS = (_Done and _Done_Rising_Setup);  // timer done OS
  _Done_Rising_Setup = !_Done;
  /*
    ---- and another positive going one-shot pulse on _Done true-to-false transition
  */
  _Done_Falling_OS = (!_Done and _Done_Falling_Setup);  // timer not done OS
  _Done_Falling_Setup = _Done;
  /*
    ----- Condition the timer running flag.
  */
  if ((_Enable or _Control) and !_Done and !_Reset) {
    _TimerRunning = true;
  } else _TimerRunning = false;

  return _Done;  // exit to caller

}  // end update function

/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
   Define various types of timers which inherit/derive from Multi_timer.
   The timers differ in functionality by their reset methods.
   ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
//                  On Delay Timer class definition
//--------------------------------------------------------------------
// A standard timer which runs when reset is false and enable is true
// It is reset otherwise.
// * current version consumes 60 bytes RAM
//--------------------------------------------------------------------

OnDelay_tmr::OnDelay_tmr(unsigned long pre)
  : Multi_timer(pre){};

// Establish reset conditions for ON delay timer
bool OnDelay_tmr::reset() {
  return (_Reset or !_Enable);
}  // End of OnDelay timer

//=================================================================
//
//                  Flasher Timer class definition
//-----------------------------------------------------------------
// This timer runs and resets itself automatically when enabled.
// It is basically an enhanced OnDelay timer. The second constructor
// argument specifies an ON time for a special output (_FlashOut)
// unique to this type. If _Enable goes false the timer is reset
// immediately.
//
// onTime must be some fraction of pre

Flasher_tmr::Flasher_tmr(unsigned long pre, unsigned long onTime)
  : Multi_timer(pre, onTime){};

bool Flasher_tmr::reset() {
  return (!_Enable or _Done);
}

bool Flasher_tmr::isFlashing() {

  // 4/2/24 : Moved _FlashOut code from update() to getFlash()
  // so that _FlashOut only applies to Flasher_tmr objects.
  //
  // Code below will turn on a common cathode LED for _OnTime
  // milliseconds when timing cycle starts.

  if (_Accumulator <= _OnTime) {
    _FlashOut = true;
  } else _FlashOut = false;

  return (_FlashOut and _Enable);
}

// 11/17/18 : Added method to runtime adjust _OnTime
// 12/18/18 : Added forced reset

void Flasher_tmr::setOnTime(unsigned long newOnTime) {
  _OnTime = newOnTime;
  _Accumulator = _Preset;  // Force a reset when new onTime loaded
}
// End of Flasher timer

//==============================================================

//              Retentive timer class definition
//--------------------------------------------------------------
// A timer which accumulates when enabled. Accumulated value is
// retained when enable is false.  This timer type is reset only
// by making the reset input true.
// * current version consumes 96 bytes RAM
//--------------------------------------------------------------

Retentive_tmr::Retentive_tmr(unsigned long pre)
  : Multi_timer(pre){};

// Establish reset conditions for retentive ON delay timer
bool Retentive_tmr::reset() {
  return (_Reset);
}  //End of Retentive timer

//==============================================================

//               Pulse Generator Timer Definition
//-----------------------------------------------------------
// A timer which runs when enabled and not reset. Resets
// itself upon reaching preset then restarts the timing cycle
// automatically as long as enable is true.
// * current version consumes 60 bytes RAM
//------------------------------------------------------------

PulseGen_tmr::PulseGen_tmr(unsigned long pre)
  : Multi_timer(pre){};

// Establish reset conditions for pulse generator timer
bool PulseGen_tmr::reset() {
  return (_Reset or _Done_Rising_OS);
}  //End of class PulseGen_tmr

//============================================================

//          Latched /Retentive/ timer class definition
//--------------------------------------------------------------
// A timer which starts when 'Setlatch' is called if reset is
// false. Once started, timer continues, even if _Enable
// subsequently goes false, until preset is reached. This timer
// type is reset only by making the reset input true.
// * current version consumes 92 bytes RAM
//--------------------------------------------------------------

Latched_tmr::Latched_tmr(unsigned long pre)
  : Multi_timer(pre){};

// Caller starts Latched timer with a call to Start with strt
// true. Once started the timer runs independently to preset.
// _Control is used to isolate the latched timer from '_Enable'
// in the base class which would override the latch. Because of
// this, a Latched Timer has no need of _Enable at all.

void Latched_tmr::Start(bool strt) {
  if (!strt) return;
  _Control = true;
}
// Establish reset conditions for self-latching ON delay timer
bool Latched_tmr::reset() {
  return (_Reset and _Done);
}  // End of class Latched_tmr

//=================================================================

//                    Watchdog timer
//---------------------------------------------------------------
// Runs when enable is true. A change of state on the 'control'
// input resets the timer and restarts the timing cycle.
// Continuous cycling of the control input at a rate faster than
// the time delay will cause the done status flag to remain
// low indefinitely.
//----------------------------------------------------------------

WatchDog_tmr::WatchDog_tmr(unsigned long pre)
  : Multi_timer(pre){};

bool WatchDog_tmr::reset() {
  /*
    Generate a positive going one-shot pulse whenever control
    input undergoes a state change.
  */
  _WD_Rising_OS = (_Control and _WD_Rising_Setup);
  _WD_Rising_Setup = !_Control;
  _WD_Falling_OS = (!_Control and _WD_Falling_Setup);
  _WD_Falling_Setup = _Control;

  return (_WD_Falling_OS or _WD_Rising_OS or _Reset);

}  // End of WatchDog timer

.h

// filename: Multi_Timer.h  6/18/19
//
// Some basic timers with a (it is hoped) simple interface.

#ifndef MULTI_TIMER_H
#define MULTI_TIMER_H

#include "Arduino.h"

class Multi_timer {

public:
  Multi_timer(unsigned long);                 //constructor declaration
  Multi_timer(unsigned long, unsigned long);  // flasher constructor
  void setEnable(bool);
  void setReset(bool);
  bool isEnabled();
  bool isReset();
  void setCtrl(bool);
  bool isDone();
  bool isRunning();
  bool isIntv();
  bool isOSRise();
  bool isOSFall();
  long pre;


protected:
  virtual bool reset();

public:
  //    Function to operate timers created under Multi_timer
  boolean update();

  //  private:
  //  protected:


public:
  unsigned long _Preset;
  bool _Reset : 1;
  bool _Enable : 1;
  bool _Done : 1;
  bool _Done_Rising_OS : 1;
  bool _Control : 1;
  unsigned long _Accumulator;


protected:
  //unsigned long _Accumulator;
  unsigned long _CurrentMillis;
  unsigned long _LastMillis;
  unsigned long _OnTime;
  bool _TimerRunning : 1;
  bool _Done_Falling_OS : 1;
  bool _Done_Rising_Setup : 1;
  bool _Done_Falling_Setup : 1;
  bool _FlashOut;

};  //end of base class Multi_timer declarations

/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
   Define various types of timers which inherit/derive from Multi_timer.
   The timers differ in functionality mainly by their reset methods.
   ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
//                  On Delay Timer class definition
//--------------------------------------------------------------------
// A standard timer which runs when reset is false and enable is true
// It is reset otherwise.
// * current version consumes 60 bytes RAM
//--------------------------------------------------------------------

class OnDelay_tmr : public Multi_timer {
public:
  OnDelay_tmr(unsigned long);
  virtual bool reset();
};  // End of class OnDelay_tmr
//------------------------------------------------------

//==============================================================

//              Retentive timer class definition
//--------------------------------------------------------------
// A timer which accumulates when enabled. Accumulated value is
// retained when enable is false.  This timer type is reset only
// by making the reset input true.
// * current version consumes 96 bytes RAM
//--------------------------------------------------------------

class Retentive_tmr : public Multi_timer {
public:
  Retentive_tmr(unsigned long);
  // Establish reset conditions for retentive ON delay timer
  virtual bool reset();
}; // End of class retentive timer
//----------------------------------------------------

//==============================================================

//               Pulse Generator Timer Definition
//-----------------------------------------------------------
// A timer which runs when enabled and not reset. Resets
// itself upon reaching preset then restarts the timing cycle
// automatically as long as enable is true.
// * current version consumes 60 bytes RAM
//------------------------------------------------------------

class PulseGen_tmr : public Multi_timer {
public:
  PulseGen_tmr(unsigned long);
  // Establish reset conditions for pulse generator timer
  virtual bool reset();
};  //End of class PulseGen_tmr
//-------------------------------------------

//============================================================

//          Latched /Retentive/ timer class definition

//--------------------------------------------------------------
// Caller starts Latched timer with a call to Start with strt
// true. Once started the timer runs independently to preset.
// _Control is used to isolate the latched timer from '_Enable'
// in the base class which would override the latch. Because of
// this, a Latched Timer has no need of _Enable at all.
// * current version consumes 92 bytes RAM

class Latched_tmr : public Multi_timer {
public:
  Latched_tmr(unsigned long);
  // Caller starts Latched timer here with a pulse signal.
  void Start(bool);

  // Establish reset conditions for self-latching ON delay timer
  virtual bool reset();
};
// End of class Latched_tmr
//-----------------------------------------------------

//=================================================================

//                    Watchdog timer
//---------------------------------------------------------------
// Runs when enable is true. A change of state on the 'control'
// input resets the timer and restarts the timing cycle.
// Continuous cycling of the control input at a rate faster than
// the time delay will cause the done status flag to remain
// low indefinitely.
//----------------------------------------------------------------

class WatchDog_tmr : public Multi_timer {
public:
  WatchDog_tmr(unsigned long);

  virtual bool reset();

private:
  bool _WD_Rising_OS : 1;
  bool _WD_Falling_OS : 1;
  bool _WD_Falling_Setup : 1;
  bool _WD_Rising_Setup : 1;
};
// End of class WatchDog_tmr
//---------------------------------------------
//
//=================================================================
//
//                  Flasher Timer class definition
//-----------------------------------------------------------------
// This timer runs and resets itself automatically when enabled.
// It is basically an enhanced OnDelay timer. The second constructor
// argument specifies an ON time for a special output (_FlashOut)
// unique to this type. If _Enable goes false the timer is reset
// immediately.
//
// onTime must be some fraction of pre


class Flasher_tmr : public Multi_timer {
public:
  Flasher_tmr(unsigned long, unsigned long);

  virtual bool reset();

  // 4/2/24 : Moved _FlashOut decision from update() to getFlash()
  // so that _FlashOut code only applies to Flasher_tmr objects.
  //

  bool isFlashing();

  // 11/17/18 : Added method to runtime adjust _OnTime
  // 12/18/18 : Added forced reset


  void setOnTime(unsigned long);
};
// end of class Flasher_tmr
//---------------------------------------------

#endif

Thanks for any help.

Class function reset() renamed per Renamed 'iterating through a series of objects' - original title: OOP, this pointer questions - #7 by dougp

While you wait, read the code of this class which has a mechanism for updating all objects:

HTH

a7

The class @gfvalvo wrote uses a private static variable. There is only one for the class, it is shared by all objects.

    static Pedal *pedalArray[maxPedals];

a7

Thanks, I'll check it out after some sleep.

Thanks! Learned a new acronym today!

I think I get most of that.  Now a new question arises.  Would that array approach also work on update() it it were virtual?  I'm imagining a loop of some sort to iterate through all the array elements updating along the way.

Easily remedied! :smiley:

OK.  

I don't see a use for calling Reset that way since each timer will have its own reasons/conditions for resetting. For instance, somewhere in the code there could be a line like

timerX.setReset( if (digital.Read(aSwitch) == LOW ));

but, maybe I'm not seeing far enough.

My interest is more doing something like that with update since every timer has a continual need for it and to relieve the user of setting up individual calls. I know they'll still happen in a loop of whatever flavor and that there'll be some overhead incurred.  I also just think it would look better to have something like,

updateAllTimers();

Yup.

I'm sure the compiler would complain and make me correct it, that was meant only as an example.

1 Like

Into the rabbit hole.

Wellll.  I struggle with some of these concepts so at my level rabbit holes are a given.

By introducing a "timer manager" class. The manager is responsible for maintaining the list of managed instances and calling update on the instances it manages. In every possible way that will improve the design.

For microcontrollers, splitting out the storage (the array of pointers to instances) can also be beneficially but does add a bit more code.

With that you could have multiple instances of manager objects that each handle their own group of timers. Using the static technique within the timer class itself limits you to one manager for all timers. Besides that, what improvements do you see it providing?

Other thoughts …. irrespective of using static array or separate manager ….

I’m partial to the link list approach over the fixed-size array only because writing code to handle creation, insertion, and deletion in linked lists is a great learning exercise. Can’t remember how many times I had to do that in my undergrad programming classes (that was back right after the Earth cooled). But, in reality, a fixed size array will work just fine for most Arduino applications. It wastes little space as it only holds pointers. That being said, on an ARM or ESP platform, I’d go with std::vector or std::list.

Speaking of deletion, your class will then need a custom destructor to handle removal of an element from the array / list when it’s deleted.

Also, I image that it would make little sense to construct one timer from another or assign one timer to another. So, the copy constructor and copy assignment operator should be deleted:

class Multi_timer {

  public:
    Multi_timer(unsigned long);                 //constructor declaration
    Multi_timer(unsigned long, unsigned long);  // flasher constructor
    Multi_timer(const &Multi_timer other) = delete;
    Multi_timer &Multi_timer(const &Multi_timer other) = delete;

Is that encourage a bad habit?

Sounds interesting.  In outline form, what might that look like?  Would a
'timer manager' class obviate @Delta_G's template class?

I don't envision removing timers during runtime.  I see that as akin to removing a digital input, as long as the program runs it needs that switch.  Is a destructor then still needed - if only for good form?

Since only update() needs to be called for each timer, I don't see that as a limitation.  Is this wrong?

No, for the reason I very clearly stated:

And then further down:

You never know what the user of a library might do. It could be that a timer has reached the end of its usefulness in the code. It could be that run-time information is required to create the timer so that it must be instantiated dynamically.

If the default destructor leaves things in a bad state, then it's really good form to provide a custom one. Dynamic deletion of a timer as above would lead to dereferencing of an invalid pointer if the array / list is not cleaned up.

It's a design choice.

Considering that the code attached above is the extent of my knowledge what should I be reading about to move forward with this 'all at a stroke' idea for the library?

Here's a skeletal example using the static method. There's one base class and one class derived from it. That, of course, can be expanded. I purposely defined the objects in setup() so that you could see destructors run when that function exits.

class BaseClass {
  public:
    BaseClass() {
      for (auto &ptr : timerPtrs) {
        if (ptr == nullptr) {
          ptr = this;
          break;
        }
      }
    }

    ~BaseClass() {
      for (auto &ptr : timerPtrs) {
        if (ptr == this) {
          Serial.println("BaseClass - Clearing Pointer");
          ptr = nullptr;
          break;
        }
      }
    }

    BaseClass(const BaseClass &other) = delete;
    BaseClass &operator=(const BaseClass &other) = delete;

    virtual void update() {
      Serial.println("Updating BaseClass");
    }

    static void updateAll() {
      for (auto &ptr : timerPtrs) {
        if (ptr != nullptr) {
          ptr->update();
        }
      }
    }

  private:
    const static size_t maxBase {20};
    static BaseClass *timerPtrs[maxBase];
};

class DerivedClass1 : public BaseClass {
  public:
    DerivedClass1() {}
    ~DerivedClass1() {
      Serial.println("Destructing DerivedClass1");
    }
    virtual void update() {
      Serial.println("Updating DerivedClass1");
    }
};

const size_t BaseClass::maxBase;
BaseClass *BaseClass::timerPtrs[maxBase] {nullptr};


void setup() {
  Serial.begin(115200);
  delay(2000);
  BaseClass base[3];
  DerivedClass1 derived1[2];
  BaseClass::updateAll();
}

void loop() {
}

Thank you @gfvalvo!   Studying now.

OK, there's a lot in this short sketch I've yet to come to grips with.  In the meantime I thought I'd start with an "All ya gotta do is..." type of thing.  I just duplicated DerivedClass1 and renamed the new code DerivedClass2 and added that to the actions in setup() in order to print a message when it's called.  Instead of working I get

exit status 1

Compilation error: 'DerivedClass2' was not declared in this scope

at line 82.

I suppose it's obvious the knowledgeable but I'm not seeing it.

// example by @gfvalvo - https://forum.arduino.cc/t/oop-this-pointer-questions/1244434/25

class BaseClass { // BaseClass defined
public:
  BaseClass() {
    for (auto &ptr : timerPtrs) {
      if (ptr == nullptr) {
        ptr = this;
        break;
      }
    }
  }

  ~BaseClass() {
    for (auto &ptr : timerPtrs) {
      if (ptr == this) {
        Serial.println("BaseClass - Clearing Pointer");
        ptr = nullptr;
        break;
      }
    }
  }

  BaseClass(const BaseClass &other) = delete;
  BaseClass &operator=(const BaseClass &other) = delete;

  virtual void update() {
    Serial.println("Updating BaseClass");
  }

  static void updateAll() { // updateAll defined
  Serial.println("running updateAll");
    for (auto &ptr : timerPtrs) {
      if (ptr != nullptr) {
        ptr->update();
      }
    }
  }

private:
  const static size_t maxBase{ 20 };
  static BaseClass *timerPtrs[maxBase];
}; // end of BaseClass

class DerivedClass1 : public BaseClass {
public:
  DerivedClass1() {} // constructor
  ~DerivedClass1() {
    Serial.println("Destructing DerivedClass1");
  }
  virtual void update() {
    Serial.println("Updating DerivedClass1");
  }
}; // end of DerivedClass1

/*
Adding another derived class
*/

class Derivedclass2 : public BaseClass {
public:
  Derivedclass2() {} // constructor
  ~Derivedclass2() {
    Serial.println("Destructing derivedclass 2");
  }
  virtual void update() {
    Serial.println("Updating derivedclass 2");
  }
}; // end of derivedclass2

const size_t BaseClass::maxBase; // maxBase defined
BaseClass *BaseClass::timerPtrs[maxBase]{ nullptr }; // array of timerPtrs defined


void setup() {
  Serial.begin(115200);
  delay(2000);
  // 1st print
  BaseClass base[3];            // prints 'updating baseclass' three times
  Serial.println();
  DerivedClass1 derived1[2];  // prints 'updating derivedclass1' two times
  DerivedClass2 derived2[2];  // prints 'updating derivedclass2' two times
  Serial.println();
  BaseClass::updateAll(); //  then prints 'baseclass - clearing pointer' for each derived class
  Serial.println("\nend - everything in setup goes out of scope\n");
  
}

void loop() {
}

I added some comments to help keep my head straight as to what's happening.