Conditional based on variable type?

What's the best way to check a variable for a certain type?

I'm trying to write a function that calculates the length of either a string or a number, so I can center-align a print to an LCD. The basic idea is to set the cursor to the half of the max LCD cols, minus half of the length in question.

I have a function that (so far) in somewhat limited situations, does the job. But it requires a bool saying if it's a string or not and I would like to remove this. It also doesn't cover floats (which isn't actually needed but heck, there must be a way to make this cool. hey?)

Here's what I have so far:

template<typename T>
void print(T t, bool isString) {
 uint16_t len; 
 
 if (isString) {
  len = strlen(t);
 }
 else {
  char buffer[16];
  len = sprintf(buffer, "%d", t);
 }

Serial.println(len);
}

void setup() {
  Serial.begin(115600);
  // Test a string
  Serial.print("Should be 14... Claims to be: ");
  print("Testing String", true);
  // Test an int
  Serial.print("Should be 4... Claims to be: ");
  print(6969, false);
}

void loop() { }

Just write two functions with the same name but different parameters. The compiler will be smart enough to call the right one. That’s how print works for example, they implemented various signatures

    size_t print(const __FlashStringHelper *);
    size_t print(const String &);
    size_t print(const char[]);
    size_t print(char);
    size_t print(unsigned char, int = DEC);
    size_t print(int, int = DEC);
    size_t print(unsigned int, int = DEC);
    size_t print(long, int = DEC);
    size_t print(unsigned long, int = DEC);
    size_t print(double, int = 2);
    size_t print(const Printable&);

I've only found information regarding declarations and casts. Do you have an example of using this in an if statement?

Yeah, I did want to ideally avoid function overloads as I think it feels cumbersome. But heck, if 'they' didn't have a better solution for print, what chance do I have! :sweat_smile:

I do understand my issue here, that C/C++ are statically typed and that we're responsible for knowing the types of our variables.

But in a 2021 world,where C++ can do seemingly anything it likes, center aligning an lcd (you would think!) should be trivial.

well, that might just be my view, but having a big list of if/else in one single function to perform different operations based on type is "ugly"... You are better off with multiple functions having different signature and the same name.... That's what it was meant for.

As a side note, C++ has a typeid operator but if I remember correctly its use is disabled by some flags the IDE is using. (fno-rtti = no real time type information)

Okay so here's the overload attempt. So far it seems to be working okay. I'm aware that it says 'print' but actually serial prints the length. Once all of this is working properly it will not print the character lengths but will actually do the printing.

Can anyone see any faults, oversights, anything unnecessary?

(Comment in/out to do the various tests)

// Char pointer / Char array
void print(const char* cp) {
  uint16_t len = 0;
  while (1) {
    char c = *cp++;
    if (c == 0) break;
    len++;
  }
  Serial.println(len);
}

// String
void print(String &s) {
  uint16_t len = s.length();
  Serial.println(len);
}

// Flash String
void print(const __FlashStringHelper *ifsh) {
  PGM_P p = reinterpret_cast<PGM_P>(ifsh);
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n++;
  }
  Serial.println(n);
}

// Integer
void print(const int& i) {
  char buffer[17];
  uint16_t len = sprintf(buffer, "%d", i);
  Serial.println(len);
}

// Float
void print (const float& f, int dec = 2) {
  if (dec > 17) return;

  char buffer[17] = {'\n'};

  dtostrf(f, 0, dec, buffer);

  char * cp = buffer;
  uint16_t len = 0;

  while (1) {
    char c = *cp++;
    if (c == 0) break;
    len++;
  }
  Serial.println(len);
}

// Double
void print (const double& f, int dec = 2) {
  if (dec > 17) return;

  char buffer[17] = {'\n'};

  dtostrf(f, 0, dec, buffer);

  char * cp = buffer;
  uint16_t len = 0;

  while (1) {
    char c = *cp++;
    if (c == 0) break;
    len++;
  }
  Serial.println(len);
}


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

  String x = "TEN CHARS!";
  // char* x = "TEN CHARS!";
  // char x[] = "TEN CHARS!";
  print(x);
  // print("TEN CHARS!");
  // print(F("TEN CHARS!"));

  // int x = 22222;
  // int x = -22222;
  // print(x);

  // float x = 49.8765432;
  // double x = 49.8765432;
  // Defaults to 2 decimal places unless specified such as below
  // print (x, 7);
}

void loop() { }


IMO, reinventing strlen() is simply making unnecessary extra work work for yourself.

Also:

void print (const float& f, int dec = 2) {

'dec' is unlikely to be negative. So, why use a signed 'int'?

Noted on the negative! It was just quick typing without considering the type fully.

Does strlen() work with all data types?

It works as described in the link I provided.

There's also the analogous strlen_P().

do you want to take into account the possibility of a nullptr argument?

you could use the strlen() function instead of your while loop for the const char* case

void print(const char* cp) {
  Serial.println(strlen(cp));
}

Similar functions exist for PROGMEM data (strlen_P or strlen_PF)

a suggestion: instead of being void the return type could be size_t and you return the number length of what has been printed?


EDIT: gfvalvo was faster :slight_smile:

Appreciate the views/advice so far!

So now we have:

size_t length(const char* cp) {
  return strlen(cp);
}

size_t length(String &s) {
  return s.length();
}

size_t length(const __FlashStringHelper *ifsh) {
  return strlen_P(reinterpret_cast<PGM_P>(ifsh));
}

size_t length(const int& i) {
  char buffer[17];
  return sprintf(buffer, "%d", i);
}

size_t length(const float& f, uint8_t dec = 2) {
  char buffer[17] = {'\n'};
  return strlen(dtostrf(f, 0, dec, buffer));
}

size_t length(const double& d, uint8_t dec = 2) {
  char buffer[17] = {'\n'};
  return strlen(dtostrf(d, 0, dec, buffer));
}

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

  // String x = "TEN CHARS!";
  // char* x = "TEN CHARS!";
  // char x[] = "TEN CHARS!";

  // Serial.println(length("TEN CHARS!"));
  // Serial.println(length(F("TEN CHARS!")));

  // int x = 22222;
  // int x = -22222;

  Serial.println(length(x));


  // float x = 49.8765432;
  // double x = 49.8765432;
  // Defaults to 2 decimal places unless specified such as below
  // Serial.println(length(x, 5));
}

void loop() { }


How's that looking?

Nice !

Great!

Well, in that case, I think this might be mission accomplished? The center-aligned LCD print.

Can I please throw this to the wolves to see if it survives?

I've added comments where needed to explain, and you'll need to simply uncomment the test cases (mostly a variable and print statement).

I'll probably pull this into a .h file if/when I want to use it.

A small gripe is the truncation favouring extra space on the left, I'd rather it be on the right.


#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);

size_t getCharLength(const char* cp) {
  return strlen(cp);
}

size_t getCharLength(String &s) {
  return s.length();
}

size_t getCharLength(const __FlashStringHelper *ifsh) {
  return strlen_P(reinterpret_cast<PGM_P>(ifsh));
}

size_t getCharLength(const int& i) {
  char buffer[17];
  return sprintf(buffer, "%d", i);
}

size_t getCharLength(const float& f, uint8_t dec = 2) {
  char buffer[17] = {'\n'};
  return strlen(dtostrf(f, 0, dec, buffer));
}

size_t getCharLength(const double& d, uint8_t dec = 2) {
  char buffer[17] = {'\n'};
  return strlen(dtostrf(d, 0, dec, buffer));
}

template<typename T>
void LCDPrint(T t, uint8_t row, bool clear = false) {
  if (clear) lcd.clear();
  lcd.setCursor(8 - (getCharLength(t) / 2), row);
  lcd.print(t);
}

template<typename T>
void LCDPrint(T &t, uint8_t p, uint8_t row, bool clear = false) {
  if (clear) lcd.clear();
  lcd.setCursor(8 - (getCharLength(t, p) / 2), row);
  lcd.print(t, p);
}

void setup() {
  Serial.begin(115600);
  lcd.init();
  lcd.backlight();
  lcd.clear();

  // Usage:
  // LCDPrint(Variable, LCD Row, Bool To Clear LCD);
  // Or...
  // LCDPrint(Variable, Decimal Places, LCD Row, Bool To Clear LCD);
  // Bool to clear LCD is defaulted to false;

  // String x = "TEN CHARS!";
  // LCDPrint(x, 0);

  // char* x = "TEN CHARS!";
  // LCDPrint(x, 0);

  // char x[] = "TEN CHARS!";
  // LCDPrint(x, 0);

  // LCDPrint(F("TEN CHARS!"), 0, true);

  // LCDPrint("TEN CHARS!", 0);

  // int x = 22222;
  // LCDPrint(x, 0);

  // int x = -22222;
  // LCDPrint(x, 0);



  // Float and double defaults to 2 decimal places unless specified.
  // float x = 49.8765432;
  // LCDPrint(x, 0);

  // Unfortunately the overload is ambiguous, so if we specify the decimals
  // we HAVE to specify true/false to clear the LCD.
  // float x = 49.8765432;
  // LCDPrint(x, 3, 0, false);

  // double x = 49.8765432;
  // LCDPrint(x, 0);
}

void loop() { }


Don't put function definitions in a .h file, just the signatures. The definitions belong in the associated .cpp file. Otherwise you risk multiple definitions for the same function.

It's likely bad practice but I do all of my (personal) external files as .h (with decl. and defs. inside), and header guard them.

If I understand it correctly, it's essentially cut/paste into the one file and should only come back to bite me if I want to use these functions from inside a different translation unit?

Maybe it's simply just safer to do the .h .cpp anyway though, so noted and thanks! :wink:

Appreciate the help with this @gfvalvo @J-M-L so if you were approaching this issue you'd not change/add anything to this?

Do you think, on the off-chance that others want to use this too... That there is an issue with having the lcd variable? (Other's may call their LiquidCrystal instance something else)...

Is it worth maybe adding:

namespace CenterAlign {

LiquidCrystal_I2C* lcd = nullptr;

template<typename T>
void LCDPrint(T t, uint8_t row, bool clear = false) {
  if (!CenterAlign::lcd) return;
// ..... etc etc including replacing lcd.print to lcd->print ...

}:

And making the user set the pointer like:

LiquidCrystal_I2C theirLCD(0x27, 16, 2);

  CenterAlign::lcd = &theirLCD;
  CenterAlign::LCDPrint(x, 1);

?

IMO, using 'const' references for passing simple numeric data types adds nothing but noise. Since C++ just does pass by value for these types, this is equivalent:

size_t getCharLength(int i) {
size_t getCharLength(float f, uint8_t dec = 2) {
size_t getCharLength(double d, uint8_t dec = 2) {

And, there's really no need for this anyway:

size_t getCharLength(float, uint8_t dec = 2) {

An argument of type float would be implicitly cast to a double. So just have that one.

1 Like

One more thought. You might be able to get rid of the free functions all together by using Template Specialization. But, my templating skills fall a little short with that.

After a coffee and a little movement... I've decided that functions related to setting a cursor position have no place interfering with printing or clearing the screen. And so I've changed the usage to be something I feel is much better suited (and less hassle, to be fair):

"CenterAlign.h" (That should and will be split into .h and .cpp)

#ifndef CENTER_ALIGN_H
#define CENTER_ALIGN_H

/*
Ironed out with some aide from 'gfvalvo' and 'J-M-L':
https://forum.arduino.cc/t/conditional-based-on-variable-type/925410/
*/

#ifndef LCD_COLS
#define LCD_COLS 16
#endif

size_t getCharLength(const char* cp) {
  return strlen(cp);
}

size_t getCharLength(String &s) {
  return s.length();
}

size_t getCharLength(const __FlashStringHelper *ifsh) {
  return strlen_P(reinterpret_cast<PGM_P>(ifsh));
}

size_t getCharLength(const int& i) {
  char buffer[17];
  return sprintf(buffer, "%d", i);
}

size_t getCharLength(const double& d, uint8_t dec = 2) {
  char buffer[17] = {'\n'};
  return strlen(dtostrf(d, 0, dec, buffer));
}

template<typename T>
size_t centerMinus(T t) {
  return (LCD_COLS / 2) - (getCharLength(t) / 2.f);
}

template<typename T>
size_t centerMinus(T t, uint8_t dec) {
  return (LCD_COLS / 2) - (getCharLength(t, dec) / 2.f);
}

#endif

And an example: (Note! #define LCD_COLS has to come before the include "CenterAlign.h" or it will default to 16 cols)

#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);

#define LCD_COLS 16
#include "CenterAlign.h"

void setup() {
  Serial.begin(115600);
  lcd.init();
  lcd.backlight();
  lcd.clear();
  
  /* 
  Usage: centerMinus(Variable); 
  For floats/doubles...  centerMinus(Variable, decimalPlaces);
  decimalPlaces defaults to 2.
  */


// Uncomment for test cases
  String x = "Centered";
  lcd.setCursor(centerMinus(x), 0);
  lcd.print(x);

  // char* x = "Centered";
  // lcd.setCursor(centerMinus(x), 0);
  // lcd.print(x);

  // char x[] = "Centered";
  // lcd.setCursor(centerMinus(x), 0);
  // lcd.print(x);

  // lcd.setCursor(centerMinus(F("Centered")), 0);
  // lcd.print(F("Centered"));

  // lcd.setCursor(centerMinus("Centered"), 0);
  // lcd.print("Centered");

  // int y = 22222;
  // lcd.setCursor(centerMinus(y), 1);
  // lcd.print(y);

  // int y = -22222;
  // lcd.setCursor(centerMinus(y), 1);
  // lcd.print(y);


// Float and double defaults to 2 decimal places unless specified like so
  float y = 49.8765432;
  lcd.setCursor(centerMinus(y, 7), 1);
  lcd.print(y, 7);

  // double y = 49.8765432;
  // lcd.setCursor(centerMinus(y), 0);
  // lcd.print(y);

}
void loop() { }