Exclude non-integers in template

I have a template class (function?) to act as a counter.  I want for it to be able to use 8-, 16-, and 32-bit integers signed and unsigned.  I have this working.

Since it’s a counter, I want to exclude any non-integer types.  I’ve looked at typeid and the assert/static assert.  A bit of searching on the forum (not hours and hours), and the compiler, tells me typeid is not supported in IDE 2.3.2.

What I've seen says use overloaded functions.  Is this the preferred way or just a workaround?.  Is there some secret to using assert/typeid?

--- .ino file ---


// 1. C++ primer on disk 6th ed. - class templates p. 830
// 2. utilizes GCC builtin function to check overflow
//      https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html

// this version moves the class to a .h file, in a new IDE tab,
// in prepration for construction of a library.

#include "niceCounter.h"  // specify local directory


niceCounter<uint16_t> ct1(5);   // instantiation as integer with preset of five
niceCounter<uint8_t> ct2(100);  // instantiation as byte integer with preset of one hundred

byte count1 = 4;  // i/o assignment of pins
byte resetPin = 2;


void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println(__FILE__);
  Serial.println("starting");
  delay(2000);
  pinMode(count1, INPUT_PULLUP);
  pinMode(resetPin, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  static unsigned long lastMillis;
  
  if (millis() != lastMillis) {  // generate a clock for the counter(s)
    ct2.countUp(true);
    lastMillis = millis();  // reset the clock
  } else {
    ct2.countUp(false);
  }

  if (ct2.isDone()) {
    ct1.countUp(true);
    ct2.reset(true);
  } else {
    ct1.countUp(false);
    ct2.reset(false);
  }

  if (ct1.isOverFlowed()) {
    digitalWrite(LED_BUILTIN, HIGH);
  } else digitalWrite(LED_BUILTIN, LOW);

  Serial.print(ct1.getAccValue());
  Serial.print("\t\t");
  Serial.println(ct2.getAccValue());

  ct1.reset(digitalRead(resetPin) ? false : true);
}

===   .h file  ===

#ifndef NICECOUNTER_H
#define NICECOUNTER_H

/*
Library form of templated counter. For signed and
unsigned integers 8-, 16-, 32-bit.
This code handles the false-true logic for user.
Preset value (to signify completion) is given at
instantiation.

Uses GCC builtin functions to detect overflow/underflow.
https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html

Provides these functions:
countUp,
countDown,
isDone,
isOverflowed,
isUnderflowed,
getAccValue,
reset
*/

template<typename T = int16_t>  // assign default type
class niceCounter {
  T accValue;
  T presetValue;
  T temp;  // ovf test result
  bool resestIn;
  bool osrUp, osrUpSetup, overflow;
  bool osrDown, osrDownSetup, underflow;

public:
  niceCounter(T pre) {  // constructor
    presetValue = pre;
  };

  ~niceCounter(){};  // destructor

  /*
      niceCounter member functions
  */

  void countUp(bool increment) {
    // Adds one to accValue when increment goes false-to-true
    // State change detection handled internally
    //
    osrUp = increment and osrUpSetup;
    osrUpSetup = !increment;
    // osrUp true for one scan when increment goes false-to-true
    if (osrUp) {
      if (__builtin_add_overflow(accValue, 1, &temp)) {
        overflow = true;
      }
      accValue++;
    }
  }

  void countDown(bool decrement) {
    // Subtracts one from accValue when increment goes false-to-true
    // State change detection handled internally
    //
    osrDown = decrement and osrDownSetup;
    osrDownSetup = !decrement;
    // osrDown true for one scan when decrement goes false-to-true
    if (osrDown) {
      if (__builtin_sub_overflow(accValue, 1, &temp)) {
        underflow = true;
      }
      accValue--;
    }
  }

  bool isOverFlowed() {
    return overflow;
  }

  bool isUnderFlowed() {
    return underflow;
  }

  void reset(bool resetIn) {
    if (resetIn) {
      accValue = 0;
      overflow = false;
      underflow = false;
    }
  }

  T getAccValue() {
    return accValue;
  }

  bool isDone() {
    return accValue >= presetValue;
  }
};  // niceCounter terminates

#endif

typeid requires RunTime Type Information, and boards disable it with the -fno-rtti flag when compiling, since that adds overhead.

What kind of non-integer are you hoping to prevent? Floating-point? Does the code compile? Does it work? From a quick glance, It would have to be a type that supports the -- operator and can be assigned zero, or it will fail to compile anyway.

Not sure what you saw about overloaded functions, but here's one way to use them with static_assert

constexpr bool allowedFoo(int) {return true;}
constexpr bool allowedFoo(char) {return false;}
constexpr bool allowedFoo(double) {return false;}

template<typename T> class Foo {
  constexpr static T x = 0;
  static_assert(allowedFoo(x));
  T field;
};

Foo<uint16_t> bar16;
Foo<int8_t> bar8;
Foo<float> barf;
Foo<double> bard;
Foo<char> barc;
Foo<unsigned char> barb;

void setup() {}

void loop() {}

It complains for three of those declarations at compile-time, with

In instantiation of 'class Foo<float>':
  required from here
error: static assertion failed
   static_assert(allowedFoo(x));
   ^~~~~~~~~~~~~

All the constexpr means that things can be evaluated at compile-time, including the static_assert of the class-static value. ("static" is an overloaded term, C++-pun intended.) It appears to take neither program nor dynamic memory -- commenting it out does not alter the bytes used (trying this on R4).

Some pretty exotic C++ stuff there! +1 to both of you. Can't you also do something with #define to limit the datatypes that can be fed to the template?

Whether or not a C++ feature is supported is not a function of the Arduino IDE version, but of the target platform and its version.

std::is_integral is available on some platforms. It provides a compile-time confirmation that its argument is an integral type. This code behaves as expected for ESP32:

Compiles Error-Free for Integral Type:

#include <type_traits>

template <typename T>
class myClass {
  static_assert(std::is_integral<T>::value, "myClass Requires Integer Type");
};

myClass<uint16_t> x;

void setup() {
}

void loop() {
}

Throws Compiler Error if float is Used:

#include <type_traits>

template <typename T>
class myClass {
  static_assert(std::is_integral<T>::value, "myClass Requires Integer Type");
};

myClass<float> x;

void setup() {
}

void loop() {
}
sketch_oct04a:5:38: error: static assertion failed: myClass Requires Integer Type
    5 |   static_assert(std::is_integral<T>::value, "myClass Requires Integer Type");
      |                                      ^~~~~
C:\Users\GFV\AppData\Local\Temp\untitled559408142.tmp\sketch_oct04a\sketch_oct04a.ino:5:38: note: 'std::integral_constant<bool, false>::value' evaluates to false
exit status 1
static assertion failed: myClass Requires Integer Type

In the same idea as @kenb4 , if you want this to work on any platform, you might have to do the type checking yourself (which is easy in this case as you know the allowed types)

You could define yourself a traits class which is just a type that carries compile-time information. Using a struct (or class ) allows you to define static members, specialize the template for specific types, and use it in static_assert or other compile-time contexts.

for example try this

template<typename T> struct is_integral_type {static constexpr bool value = false; };
template<> struct is_integral_type<uint8_t> { static constexpr bool value = true; };
template<> struct is_integral_type<int8_t> { static constexpr bool value = true; };
template<> struct is_integral_type<uint16_t> { static constexpr bool value = true; };
template<> struct is_integral_type<int16_t> { static constexpr bool value = true; };
template<> struct is_integral_type<uint32_t> { static constexpr bool value = true; };
template<> struct is_integral_type<int32_t> { static constexpr bool value = true; };

// -----------------------

template<typename T = int16_t>
class niceCounter {
  static_assert(is_integral_type<T>::value, "niceCounter requires an integral type");

  T accValue = 0;
  T presetValue;
  T temp;
  bool osrUp = false, osrUpSetup = false, overflow = false;

public:
  niceCounter(T pre) : presetValue(pre) {}

  void countUp(bool increment) {
    osrUp = increment && osrUpSetup;
    osrUpSetup = !increment;
    if (osrUp) {
      if (__builtin_add_overflow(accValue, 1, &temp)) {
        overflow = true;
      }
      accValue++;
    }
  }

  bool isOverFlowed() { return overflow; }
  T getAccValue() { return accValue; }
  bool isDone() { return accValue >= presetValue; }
  void reset(bool resetIn) { if (resetIn) { accValue = 0; overflow = false; } }
};

// -----------------------


niceCounter<uint8_t> counter(10);


void setup() {
  Serial.begin(115200);
  Serial.println("niceCounter demo on Uno");
}

void loop() {
  static unsigned long lastMillis = 0;
  if (millis() - lastMillis > 500) { // increment every 0.5s
    counter.countUp(true);
    lastMillis = millis();
  } else {
    counter.countUp(false);
  }

  Serial.print("Count: ");
  Serial.print(counter.getAccValue());
  Serial.print("\tDone: ");
  Serial.println(counter.isDone() ? "Yes" : "No");

  if (counter.isDone()) {
    counter.reset(true);
  }

  delay(100);
}

and try to change

niceCounter<uint8_t> counter(10);

into

niceCounter<float> counter(10);

and see the static_assert do its job.

My bad, I'm running this on a NANO and IDE 2.3.2.

Yes, that's the error I saw.

YIelds "Compilation error: type_traits: No such file or directory"

Anything other than 8-, 16-, 32-bit signed or unsigned integers. No floats, doubles, chars, bools.

Examining/running @J-M-L 's example now.

I've also been wondering whether a template can restrict the types it accepts, perhaps even with forcing an implicit cast to one of the accepted types. I guess, essentially not :frowning:

You'd thing that list of integral types would be sufficient. But, depending on the platform, it seems to fail for types like int, unsigned int, or char.

That would be expected for an AVR platform.

I'd say "definitely maybe" or "a qualified yes" or "it depends" :grin:

Yes you would have to list everything you want to authorize

I couldn't see how to adapt @kenb4's example so I marked the example posted by J-M-L as solution because I could get it to work (and I do have a Trinket lying here on which I might someday want to have a counter).

The examples and guidance are much appreciated.

Running on an UNO

.ino:

// template code from that offered by J-M-L at
// https://forum.arduino.cc/t/exclude-non-integers-in-template/1409112

// 1. C++ primer on disk 6th ed. - class templates p. 830
// 2. utilizes GCC builtin function to check over/underflow
//    https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html

// this version moves all template/class code to a header file
// in a new IDE tab, and is called as a library.


#include "niceCounter.h"  // specify local directory

//---------------------------

niceCounter<int8_t> counter(10);
niceCounter<> counter2(10);  // type defaults to int
niceCounter<uint32_t> badCounter(3);


void setup() {
  Serial.begin(115200);
  // verify widths of instantiated counters
  Serial.println("niceCounter demo on Uno");
  Serial.print("size  counter2 - ");
  Serial.println(counter2.counterSize);
  Serial.print("size  counter - ");
  Serial.println(counter.counterSize);
  Serial.print("size  badcounter - ");
  Serial.println(badCounter.counterSize);
  delay(4500);
}

void loop() {
  static unsigned long lastMillis = 0;
  if (millis() - lastMillis > 500) {  // increment every 0.5s
    counter.countUp(true);
    lastMillis = millis();
  } else {
    counter.countUp(false);
  }

  Serial.print("Count: ");
  Serial.print(counter.getAccValue());
  Serial.print("\tDone: ");
  Serial.print(counter.isDone() ? "Yes" : "No");
  Serial.print("\t");
  counter.isDone() ? counter2.countUp(true) : counter2.countUp(false);
  Serial.println(counter2.getAccValue());

  if (counter.isDone()) {
    counter.reset(true);
  }

  delay(100);
}

.h:

#ifndef NICECOUNTER_H
#define NICECOUNTER_H

/*
Library form of templated counter. For signed and
unsigned integers 8-, 16-, 32-bit.
This code handles the false-true logic for user.
Preset value (to signify completion) is given at
instantiation.

Uses GCC builtin functions to detect overflow/underflow.
https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html

Provides these functions:
countUp,
countDown,
isDone,
isOverflowed,
isUnderflowed,
getAccValue,
reset
*/

// clang-format off
template<typename T> struct is_integral_type
  { static constexpr bool value = false; };

template<> struct is_integral_type<uint8_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<int8_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<uint16_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<int16_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<uint32_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<int32_t>
  { static constexpr bool value = true; };

template<typename T = int16_t>  // assign default type

class niceCounter {
  T accValue=0;
  T presetValue;
  T temp=0;  // ovf/uf test result
  bool resestIn;
  bool osrUp, osrUpSetup, overflow;
  bool osrDown, osrDownSetup, underflow;

public:

T counterSize=sizeof(presetValue);

  niceCounter(T pre) {  // constructor
    presetValue = pre;
  };

  ~niceCounter(){};  // destructor

  /*
      niceCounter member functions
  */

  void countUp(bool increment) {
    // Adds one to accValue when increment goes false-to-true
    // State change detection handled internally
    //
    osrUp = increment and osrUpSetup;
    osrUpSetup = !increment;
    // osrUp true for one scan when increment goes false-to-true
    if (osrUp) {
      if (__builtin_add_overflow(accValue, 1, &temp)) {
        overflow = true;
      }
      accValue++;
    }
  }

  void countDown(bool decrement) {
    // Subtracts one from accValue when decrement goes false-to-true
    // State change detection handled internally
    //
    osrDown = decrement and osrDownSetup;
    osrDownSetup = !decrement;
    // osrDown true for one scan when decrement goes false-to-true
    if (osrDown) {
      if (__builtin_sub_overflow(accValue, 1, &temp)) {
        underflow = true;
      }
      accValue--;
    }
  }

  bool isOverFlowed() {
    return overflow;
  }

  bool isUnderFlowed() {
    return underflow;
  }

  void reset(bool resetIn) {
    if (resetIn) {
      accValue = 0;
      overflow = false;
      underflow = false;
    }
  }

  T getAccValue() {
    return accValue;
  }

  bool isDone() {
    return accValue >= presetValue;
  }
};  // niceCounter terminates

#endif

Your posted code never uses the is_integral_type templated struct or the static_assert which are key to how @J-M-L's solution works.

Hmm. I tested it by substituting float for one of the types and it gives an error -

niceCounter<float> counter(10);

In file included from C:\Users\User\OneDrive\Documents\Arduino\template_counter_v1_6_with_jml_type_checking\template_counter_v1_6_with_jml_type_checking.ino:12:0:
C:\Users\User\OneDrive\Documents\Arduino\template_counter_v1_6_with_jml_type_checking\niceCounter.h: In instantiation of 'void niceCounter<T>::countUp(bool) [with T = float]':
C:\Users\User\OneDrive\Documents\Arduino\template_counter_v1_6_with_jml_type_checking\template_counter_v1_6_with_jml_type_checking.ino:37:25:   required from here
C:\Users\User\OneDrive\Documents\Arduino\template_counter_v1_6_with_jml_type_checking\niceCounter.h:80:34: error: argument 1 in call to function '__builtin_add_overflow' does not have integral type
       }
                                  ^       

exit status 1

Compilation error: argument 1 in call to function '__builtin_add_overflow' does not have integral type

I think that's due to this error:

.... not from the mechanism that @J-M-L defined. Your error code doesn't show that the static_assert failed, because you never use it!

Ah, I see.  Somehow this line:

static_assert(is_integral_type<T>::value, "niceCounter requires an integral type");

got left out of the version I worked with - and posted.  It can't have helped either to have set this aside for awhile.

I wondered about this because I also substituted type char and did not get an error with my posted code.  I assumed :grimacing: char was being interpreted as an integer type and all was good.  Adding the static_assert now returns the proper compilation error message with type char.

Good catch @gfvalvo !   Thanks!

#ifndef NICECOUNTER_H
#define NICECOUNTER_H

/*
Library form of templated counter. For signed and
unsigned integers 8-, 16-, 32-bit.
This code handles the false-true logic for user.
Preset value (to signify completion) is given at
instantiation.

Uses GCC builtin functions to detect overflow/underflow.
https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html

Provides these functions:
countUp,
countDown,
isDone,
isOverflowed,
isUnderflowed,
getAccValue,
reset
*/

// clang-format off
template<typename T> struct is_integral_type
  { static constexpr bool value = false; };

template<> struct is_integral_type<uint8_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<int8_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<uint16_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<int16_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<uint32_t>
  { static constexpr bool value = true; };

template<> struct is_integral_type<int32_t>
  { static constexpr bool value = true; };

template<typename T = int16_t>  // assign default type

// clang-format on

class niceCounter {
  static_assert(is_integral_type<T>::value, "niceCounter requires an integral type");
  T accValue = 0;
  T presetValue;
  T temp = 0;  // ovf/uf test result
  bool resestIn;
  bool osrUp, osrUpSetup, overflow;
  bool osrDown, osrDownSetup, underflow;

public:

  T counterSize = sizeof(presetValue);

  niceCounter(T pre) {  // constructor
    presetValue = pre;
  };

  ~niceCounter(){};  // destructor

  /*
      niceCounter member functions
  */

  void countUp(bool increment) {
    // Adds one to accValue when increment goes false-to-true
    // State change detection handled internally
    //
    osrUp = increment and osrUpSetup;
    osrUpSetup = !increment;
    // osrUp true for one scan when increment goes false-to-true
    if (osrUp) {
      if (__builtin_add_overflow(accValue, 1, &temp)) {
        overflow = true;
      }
      accValue++;
    }
  }

  void countDown(bool decrement) {
    // Subtracts one from accValue when decrement goes false-to-true
    // State change detection handled internally
    //
    osrDown = decrement and osrDownSetup;
    osrDownSetup = !decrement;
    // osrDown true for one scan when decrement goes false-to-true
    if (osrDown) {
      if (__builtin_sub_overflow(accValue, 1, &temp)) {
        underflow = true;
      }
      accValue--;
    }
  }

  bool isOverFlowed() {
    return overflow;
  }

  bool isUnderFlowed() {
    return underflow;
  }

  void reset(bool resetIn) {
    if (resetIn) {
      accValue = 0;
      overflow = false;
      underflow = false;
    }
  }

  T getAccValue() {
    return accValue;
  }

  bool isDone() {
    return accValue >= presetValue;
  }
};  // niceCounter terminates

#endif

Presumably, that's because the char type is supported by __builtin_add_overflow (unlike float type). It failed when you added the static_assert because char is not one of the enumerated types in the template specializations for is_integral_type. So, the default case returns false. char is in fact a valid integer type (typically single-byte), so you could add a specialization for it if you wished. However that may be more trouble than it's worth as whether char is treated as signed or unsigned is implementation-dependent.