Structure/Union or Class Template to accept int, float, or String

I am working to improve my programming game. I can write functional code but want to step up my game to use more OOP methods.(no pun there)
My current project is managing the fields of an oled display. I need a data structure which holds the lastValue, which could be either an int, float, or String. Then there are a few static values for the row and column position of that data. I have tried a number of approaches. So far, Armstrong is easiest and works perfectly, but does not use any classes or structures, andI really want to up my game. A struct with a union is fine, but I had to add another field for the data type, and then test it in order to display it correctly. The template class I create works great for int and float, but I have been spinning around the axle for days trying to get something to work with String data also. Again, the idea is that that lastValue field needs to be able to except what is thrown at it, int, float, or String. Here are the two code examples. The second one contains info on the error I am getting. The first one works, but I am sure there has to be a better way to do this. THANKS FOR ANY POINTERS (no pun again) IN ADVANCE.

/*
     Objective:  create an array containing structure where the one value can be either an int, float, or char array (string).
                 then be able to use that first field appropriately based on its data type

     Solution here adds a field type variable to the structure to flag to data type of the int/float/char variable.
              This is awkward, high and high overhead because it introduces tests for the fieldtype.

              THERE MUST BE A BETTER WAY
*/

struct fldData {
  char fldType;  // 'I' for interger, 'F' for float, 'C' for char
  union {
    int lastInt;
    float lastFloat;
    char lastString[14];
  } lastData;
  uint8_t col;
  uint8_t row;
  uint8_t width;
} LCDfield[10];


void dataPrint(void) {
  for (int i = 1; i <= 3; i++) {
    //Serial.print(LCDfield[i].lastData); // HOW CAN PRINT WHATEVER FIELD IS THERE?
    if (LCDfield[i].fldType == 'I') {
      Serial.print("lastInt ");
      Serial.print(i);
      Serial.print(": ");
      Serial.print(LCDfield[i].lastData.lastInt);
    }
    if (LCDfield[i].fldType == 'F')  {
      Serial.print("lastFloat ");
      Serial.print(i);
      Serial.print(": ");
      Serial.print(LCDfield[i].lastData.lastFloat);
    }
    if (LCDfield[i].fldType == 'C') {
      Serial.print("lastString ");
      Serial.print(i);
      Serial.print(": ");
      Serial.print(LCDfield[i].lastData.lastString);
    }

    Serial.print(", ");
    Serial.print(LCDfield[i].col);
    Serial.print(", ");
    Serial.print(LCDfield[i].row);
    Serial.print(", ");
    Serial.print(LCDfield[i].width);
    Serial.println();
  }
}


void setup() {
  Serial.begin(115200);
  Serial.println("Serial Out Started");

  //              fldType  dataField        col  row   width
  LCDfield[1] =  {   'I',     int(99),        42,   1,   36  };
  LCDfield[1].lastData.lastInt = { 99 };  // override the second value by writing directly to it.
  LCDfield[2] =  {   'F',   99.99,          42,   2,   56  };
  LCDfield[2].lastData.lastFloat = { 99.99 };
  LCDfield[3] =  {   'C', 'This String', 36,   3,   14 * 7};
  strcpy (LCDfield[3].lastData.lastString, "This String" );

  dataPrint();
}



void loop() {
  // do nothing here
}

AND the class template approach, which does not work for String data.

/*
 * Objective: set up a class with a template for one of the variables in the class such that the variable can be passes an int, float, or String
 * 
 * CAN'T GET THIS TO WORK FOR WHERE T data type is a String
 */
float Volts = 12.5;
int   Amps = -5;

//Template Class

template <class T>
class LCDfield {
  public:
    //T  lastValue = '          ';  // this is the variable which must be able to accept an int, float, or String
    T lastValue;
    uint8_t    col;
    uint8_t    row;
    
  public:
    LCDfield(T lastValue, uint8_t col, uint8_t row)
    {
      this->lastValue = lastValue;
      this->col = col;
      this->row = row;
    }
    void Update(T);
}; // don't forget the semicolon at the end of the class


//Template Function

template <class T>
void LCDfield<T>::Update(T newValue) {
  //extern void dataPrint(void);
  this->lastValue = newValue;
}

// create instances name  lastValue      col row
LCDfield <float>   Field1(Volts,          1,  2);
LCDfield <int>     Field2(Amps,           2,  4);

//THE FOLLOWING LINE WILL NOT COMPILE
//LCDfield <String>  Field3('ThisMessage',  3,  6);  //DOES NOT WORK.   
                                            //error: converting to 'String' from initializer list would use explicit constructor 'String::String(int, unsigned char)'
                                            //No idea what to do with this error
void dataPrint(void) {
      
    Serial.print("Field1 ");
    Serial.print(Field1.lastValue);
    Serial.print(", ");
    Serial.print(Field1.col);
    Serial.print(", ");
    Serial.println(Field1.row);
    
    Serial.print("Field2 ");
    Serial.print(Field2.lastValue);
    Serial.print(", ");
    Serial.print(Field2.col);
    Serial.print(", ");
    Serial.println(Field2.row);

/*
    Serial.print("Field3 ");
    Serial.print(Field3.lastValue);
    Serial.print(", ");
    Serial.print(Field3.col);
    Serial.print(", ");
    Serial.println(Field3.row);
*/
    Serial.println(); Serial.println();
  }
  
void setup() {
  Serial.begin(115200);
  Serial.println("Serial Out Started");

  dataPrint();

  Field1.Update(++Volts);
  Field2.Update(++Amps);

// REMOVE COMMENT MARKS BELOW ONCE ERROR ON LINE 43 IS FIXED
//  Field3.Update("SecondString");

  dataPrint();
}
  
void loop() {
  //do nothing
}

Perhaps the answer to what you’re looking for lies in polymorphism:

class FldData {
  public:
    FldData(uint8_t c, uint8_t r, uint8_t w) : col(c), row(r), width(w) {}
    virtual void printData(Print &p) = 0;

  protected:
    void printInfo(Print &p) {
      p.print(col);
      p.print(", ");
      p.print(row);
      p.print(", ");
      p.print(width);
    }

  private:
    uint8_t col;
    uint8_t row;
    uint8_t width;
};

class Int32FldData: public FldData {
  public:
    Int32FldData(int32_t i, uint8_t c, uint8_t r, uint8_t w) : FldData(c, r, w), intVal(i) {}
    virtual void printData(Print &p) {
      p.print("lastInt ");
      p.print(": ");
      p.print(intVal);
      p.print(", ");
      printInfo(p);
    }
  private:
    int32_t intVal;
};

class FloatFldData: public FldData {
  public:
    FloatFldData(float f, uint8_t c, uint8_t r, uint8_t w) : FldData(c, r, w), floatVal(f) {}
    virtual void printData(Print &p) {
      p.print("lastFloat ");
      p.print(": ");
      p.print(floatVal);
      p.print(", ");
      printInfo(p);
    }
  private:
    float floatVal;
};

class StringFldData: public FldData {
  public:
    StringFldData(const char *s, uint8_t c, uint8_t r, uint8_t w) : FldData(c, r, w) {
      strncpy(charVal, s, numChars);
      charVal[numChars - 1] = '\0';
    }
    virtual void printData(Print & p) {
      p.print("lastString ");
      p.print(": ");
      p.print(charVal);
      p.print(", ");
      printInfo(p);
    }
  private:
    static constexpr size_t numChars = 14;
    char charVal[numChars];
};

Int32FldData int32FldData1(99, 42, 1, 36);
FloatFldData floatFldData1(99.99, 42, 2, 56);
StringFldData stringFldData1("This String",  36,   3,   14 * 7);

FldData *fields[] = {&int32FldData1, &floatFldData1, &stringFldData1};
constexpr size_t numFields = sizeof(fields) / sizeof(fields[0]);

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Serial Out Started");
  for (size_t i = 0; i < numFields; i++) {
    fields[i]->printData(Serial);
    Serial.println();
  }
}

void loop() {
}

Serial Monitor Output:

Serial Out Started
lastInt : 99, 42, 1, 36
lastFloat : 99.99, 42, 2, 56
lastString : This String, 36, 3, 98

petedd:
AND the class template approach, which does not work for String data.

//THE FOLLOWING LINE WILL NOT COMPILE

//LCDfield  Field3(‘ThisMessage’,  3,  6);  //DOES NOT WORK.  
                                           //error: converting to ‘String’ from initializer list would use explicit constructor ‘String::String(int, unsigned char)’
                                           //No idea what to do with this error

You should be using “ThisMessage”, a string constant, and not ‘ThisMessage’, a character constant. I would have expected you to get a warning that a character constant can’t be more than two characters. make sure your warning level is set to ‘All’ in Preferences.

johnwasser:
You should be using "ThisMessage", a string constant, and not 'ThisMessage', a character constant. I would have expected you to get a warning that a character constant can't be more than two characters. make sure your warning level is set to 'All' in Preferences.

Hi John. Thank you. I did also try "ThisMessage" but that did not help.

gfvalvo:
Perhaps the answer to what you’re looking for lies in polymorphism

Thank you very much gfvalvo. I will have to study this - a lot, as it makes my head spin - but I will put the work in. Thank you for putting in the work to help me move in the right direction.

petedd:

johnwasser:
You should be using “ThisMessage”, a string constant, and not ‘ThisMessage’, a character constant. I would have expected you to get a warning that a character constant can’t be more than two characters. make sure your warning level is set to ‘All’ in Preferences.

Hi John. Thank you. I did also try “ThisMessage” but that did not help.

That’s strange. When I try that it compiles without error or warning and works perfectly. All I did was change the quotes and un-comment the “Field3” lines that were commented out. The results on Serial Monitor are:

Serial Out Started
Field1 12.50, 1, 2
Field2 -5, 2, 4
Field3 ThisMessage, 3, 6


Field1 13.50, 1, 2
Field2 -4, 2, 4
Field3 SecondString, 3, 6

This is the sketch with those few changes:

/*
   Objective: set up a class with a template for one of the variables in the class such that the variable can be passes an int, float, or String

   CAN'T GET THIS TO WORK FOR WHERE T data type is a String
*/
float Volts = 12.5;
int   Amps = -5;

//Template Class

template <class T>
class LCDfield
{
  public:
    //T  lastValue = '          ';  // this is the variable which must be able to accept an int, float, or String
    T lastValue;
    uint8_t    col;
    uint8_t    row;

  public:
    LCDfield(T lastValue, uint8_t col, uint8_t row)
    {
      this->lastValue = lastValue;
      this->col = col;
      this->row = row;
    }
    void Update(T);
}; // don't forget the semicolon at the end of the class


//Template Function

template <class T>
void LCDfield<T>::Update(T newValue)
{
  //extern void dataPrint(void);
  this->lastValue = newValue;
}

// create instances name  lastValue      col row
LCDfield <float>   Field1(Volts,          1,  2);
LCDfield <int>     Field2(Amps,           2,  4);

//THE FOLLOWING LINE WILL NOT COMPILE
LCDfield <String>  Field3("ThisMessage",  3,  6);  //DOES NOT WORK.
//error: converting to 'String' from initializer list would use explicit constructor 'String::String(int, unsigned char)'
//No idea what to do with this error

void dataPrint(void)
{

  Serial.print("Field1 ");
  Serial.print(Field1.lastValue);
  Serial.print(", ");
  Serial.print(Field1.col);
  Serial.print(", ");
  Serial.println(Field1.row);

  Serial.print("Field2 ");
  Serial.print(Field2.lastValue);
  Serial.print(", ");
  Serial.print(Field2.col);
  Serial.print(", ");
  Serial.println(Field2.row);

  Serial.print("Field3 ");
  Serial.print(Field3.lastValue);
  Serial.print(", ");
  Serial.print(Field3.col);
  Serial.print(", ");
  Serial.println(Field3.row);

  Serial.println(); Serial.println();
}

void setup()
{
  Serial.begin(115200);
  Serial.println("Serial Out Started");

  dataPrint();

  Field1.Update(++Volts);
  Field2.Update(++Amps);

  // REMOVE COMMENT MARKS BELOW ONCE ERROR ON LINE 43 IS FIXED
  Field3.Update("SecondString");

  dataPrint();
}

void loop()
{
  //do nothing
}

johnwasser:
That's strange. When I try that it compiles without error or warning and works perfectly. All I did was change the quotes and un-comment the "Field3" lines that were commented out. The results on Serial Monitor are:

Hi John. Apologies. You are correct. I must have been thinking of a prior edit to another try at this. Thanks again!

Got everything working “on its own” but then when I went to integrate into my larger program, I hit a snag. One of the libraries (the oled functions) I use throws a compile error if it “might” get passed a String. The code would not hit this line (see comments below in the .Update function), but I can’t compile this that line there (and I need it). (It’s commented out and the code below runs with just Serial.print instead. So, I am moving from String to a char array, which I like for memory stability reasons of course. I can only pass to the template class function (.Update) a pointer to the char array. If I try to pass the variable by name, it will not compile (again, see errors in the comments in the code below). When I pass a pointer to the array though, only the first character of the array prints. I imagine this is an basic principle that I am missing, but I am sure missing it.

char  chargingStateString[14];
float voltValue = 12.41;
int   ampValue = 95;


// LCD SCREEN FORMAT CONSTANTS AND VARIABLES
#define screen2PixelsPerChar 7

//Top row is 0
#define screen2RowVolt      1
#define screen2RowAmp       2
#define screen2RowState     7

//Column variable is number of pixels
#define screen2ColAlt       6 *screen2PixelsPerChar
#define screen2ColBat       13*screen2PixelsPerChar
#define screen2ColState     0

//Erase variable is number of characters
#define screen2EraseVolt        6
#define screen2EraseAmp         6
#define screen2EraseState       14

//Format is single character to follow output
#define screen2FormatVolt       'V'
#define screen2FormatAmp        'A'
#define screen2FormatState      ' '


// END LCD SCREEN FORMAT CONSTANTS AND VARIABLES

char nullString[20] = "null string";

//Template Class

template <class T>
class LCDfield {
    //private:
  public:
    T  lastValue;
    uint8_t    column;
    uint8_t    row;
    uint8_t    eraseWidth;
    uint8_t    Digits;  //number of decimal places
    char       Format;

  public:
    LCDfield(T lastValue, uint8_t column,  uint8_t row,  uint8_t eraseWidth,  uint8_t Digits, char Format)
    {
      this->lastValue = lastValue;
      this->column = column;
      this->row = row;
      this->eraseWidth = eraseWidth;
      this->Digits = Digits;
      this->Format = Format;
    }
    void Update(T);
}; // don't forget the semicolon at the end of the class


//Template Function

template <class T>
void LCDfield<T>::Update(T newValue) {
  if (newValue != lastValue) {
    //lastValue = newValue;
    this->lastValue = newValue;
    //oled.clearField(column, row, eraseWidth);
    if (Digits >= 1) {
      //oled.print(newValue, Digits); // this line will NOT compile if newValue is of type String, so we try a char array
      Serial.print(newValue, Digits);
    }
    else {
      //oled.print(newValue);  // now passing *chargingStateString, only the first letter of the char array is printed
      Serial.print(newValue);
    }
    Serial.println(Format);
  }
}

// construct instances
//       <type>  dataset    (T lastValue, uint8_t column,       uint8_t row,        uint8_t eraseWidth,  uint8_t Digits, char Format)
LCDfield <float> LCDaltVolts(2.0,         screen2ColAlt,        screen2RowVolt,       screen2EraseVolt,       2,        screen2FormatVolt  );
LCDfield <int>   LCDaltAmps(1,            screen2ColAlt,        screen2RowAmp,        screen2EraseAmp,        0,        screen2FormatAmp   );
LCDfield <char>  LCDState( *nullString,   screen2ColState,      screen2RowState,      screen2EraseState,      0,        screen2FormatState );


void setup() {

  Serial.begin(115200);
  delay(100);
  Serial.println("Serial Out Started");
  Serial.println();

  strcpy(chargingStateString, "FORCED FLOAT ");

  Serial.print("These are my variables:   voltValue: ");
  Serial.print(voltValue);
  Serial.print(",   ampValue: ");
  Serial.println(ampValue);
  

  strcpy(chargingStateString, "FORCED FLOAT ");
  
  Serial.print("   chargingStateString: ");
  Serial.print(chargingStateString);
  Serial.print(",   *chargingStateString: ");
  Serial.print(*chargingStateString);
  Serial.println();  Serial.println();  Serial.println();

  Serial.println("These are the values printed from the template class function .Update");
  LCDaltVolts.Update(voltValue);
  LCDaltAmps.Update(ampValue);
  
  strcpy(chargingStateString, "FORCED FLOAT ");
  LCDState.Update(*chargingStateString);

//  LCDState.Update(chargingStateString); //compile error: invalid conversion from 'char*' to 'char' [-fpermissive]

  LCDaltVolts.Update(voltValue);
  LCDaltAmps.Update(ampValue);

}

void loop() {
  //do nothing
}

This is the output: Advice? Thanks!

Serial Out Started

These are my variables:   voltValue: 12.41,   ampValue: 95
   chargingStateString: FORCED FLOAT ,   *chargingStateString: F


These are the values printed from the template class function .Update
12.41V
95A
F

you define chargingStateString as an a array of 14 charschar  chargingStateString[14];
in C or C++ that makes chargingStateString a char* pointer, pointing to the start of the array.

when you do  Serial.print(*chargingStateString);you are dereferencing the pointer, so you are passing to the print method just a char (the one at position 0 in the array), no longer the pointer to the 1st char, hence print will just output that char.

if you were to call  Serial.print(chargingStateString);then the print method gets a char* parameter and it means it will expect a well formatted cString and will print all the bytes starting from that address as ASCII char until it finds a NULL char.

petedd:
One of the libraries (the oled functions) I use throws a compile error if it "might" get passed a String.

I'm surprised that the library doesn't inherit from the Print class, and thus would know how to print an object of the String class. Post a link to the library.

J-M-L:
you define chargingStateString as an a array of 14 chars

char  chargingStateString[14];

in C or C++ that makes chargingStateString a char* pointer, pointing to the start of the array.
...

Thanks for your reply J-M-L. What you state here pretty much confirms what my code demonstrates but it still leave me wondering how to pass chargingStateString to the .Update function. Note the error I get when I try.

//  LCDState.Update(chargingStateString); //compile error: invalid conversion from 'char*' to 'char' [-fpermissive]

How do I get around that?

LCDState is created with a char type for the template LCDfield <char>  LCDState....so your Update() method does take a char as a parameter, not a char*

gfvalvo:
I'm surprised that the library doesn't inherit from the Print class, and thus would know how to print an object of the String class. Post a link to the library.

Thanks for your reply gfvalvo. I looked at the library and I think what I am seeing is that it inherits the Print class but I do get that error when I try to compile with this line when passing a String through the .Update function:

 //oled.print(newValue, Digits); // this line will NOT compile if newValue is of type String, so we try a char array

Now, again, running the program, this line would never be reached with a String since the 'if' right before it is only executed if I also pass a Digits value >=1 and I only do that when passing a float value.

Here is a link to the library.

J-M-L:
LCDState is created with a char type for the template

LCDfield <char>  LCDState....

so your Update() method does take a char as a parameter, not a char*

OK. Again, you are correct. But how do I fix my problem?

I tried creating LCDState with <char*> and tried that with

LCDState.Update(*chargingStateString);

and, alternatively with

LCDState.Update(*chargingStateString);

and none of those combinations compile.

Am I being dumb here? What am I not getting?

petedd:
Thanks for your reply gfvalvo. I looked at the library and I think what I am seeing is that it inherits the Print class but I do get that error when I try to compile with this line when passing a String through the .Update function:

It does indeed inherit from Print. So, the problem is not that it doesn't know how to print a String object. It's more likely that you're doing something wrong with the templating for function invocation. For example, it makes no sense to try to print a String object while supplying the number of digits. That only makes sense for a numeric argument. Thus, there is no overload of the print() function for it.

As a simple test, for get the classes and templates and just try:

String s = "Hello World";
oled.print(s);

If that works, there is no problem with the oled library and Strings. The problem would be with your usage.

gfvalvo:
I'm surprised that the library doesn't inherit from the Print class, and thus would know how to print an object of the String class. Post a link to the library.

Testing this, the .print class does not accept a String if the second variable is set.

    String s = "This String";

    Serial.print (s, 0);   // error:  'string' was not declared in this scope

So the inheritance of the class is consistent.

Still don't know how to get around this... the 'if' in my code, that "gets around" the call if a String is passed, does not prevent the compiler error.

In the class definition you have an instance variable of type T

   T lastValue;

so when you instantiate

LCDfield <char>  LCDState....

the memory at the back of LCDState is big enough just for a char.

the way your class work, it does not know how to deal with “deep” (non standard types) objects when you update the content as you simply do an assignment

void LCDfield<T>::Update(T newValue) {
  //extern void dataPrint(void);
  this->lastValue = newValue;
}

and those don’t work to copy the content of an array. You would have to implement an array class with a copy method. But if your cStrings are constant, then you could instantiate your template with a const char*

try this, just typed here based on your code above, but should work.

/*
   Objective: set up a class with a template for one of the variables in the class such that the variable can be passes an int, float, or String

THIS SHOULD WORK NOW :) 

*/
float Volts = 12.5;
int   Amps = -5;

//Template Class

template <class T>
class LCDfield {
  public:
    //T  lastValue = '          ';  // this is the variable which must be able to accept an int, float, or String
    T lastValue;
    uint8_t    col;
    uint8_t    row;

  public:
    LCDfield(T lastValue, uint8_t col, uint8_t row)
    {
      this->lastValue = lastValue;
      this->col = col;
      this->row = row;
    }
    void Update(T);
}; // don't forget the semicolon at the end of the class


//Template Function

template <class T>
void LCDfield<T>::Update(T newValue) {
  //extern void dataPrint(void);
  this->lastValue = newValue;
}

// create instances name  lastValue      col row
LCDfield <float>   Field1(Volts,          1,  2);
LCDfield <int>     Field2(Amps,           2,  4);

const char* hello = "Hello";
const char* goodbye = "Good bye";
LCDfield <const char*>   Field3(hello,           2,  4);

void dataPrint(void) {

  Serial.print("Field1\t");
  Serial.print(Field1.lastValue);
  Serial.print(", ");
  Serial.print(Field1.col);
  Serial.print(", ");
  Serial.println(Field1.row);

  Serial.print("Field2\t");
  Serial.print(Field2.lastValue);
  Serial.print(", ");
  Serial.print(Field2.col);
  Serial.print(", ");
  Serial.println(Field2.row);

  Serial.print("Field3\t");
  Serial.print(Field3.lastValue);
  Serial.print(", ");
  Serial.print(Field3.col);
  Serial.print(", ");
  Serial.println(Field3.row);

  Serial.println(); Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.println("Default Values");
  dataPrint();

  Field1.Update(Volts + 1);
  Field2.Update(++Amps);
  Field3.Update(goodbye);
  Serial.println("New Values");
  dataPrint();
}

void loop() {}

this should work as the lastValue instance variable will be of a known basic type (const char*) and so you can assign a new pointer to it.

gfvalvo:
It does indeed inherit from Print. So, the problem is not that it doesn't know how to print a String object. It's more likely that you're doing something wrong with the templating for function invocation. For example, it makes no sense to try to print a String object while supplying the number of digits. That only makes sense for a numeric argument. Thus, there is no overload of the print() function for it.

We agree. But I need the other call to .print in my code to handle the formatting of the number of decimal places for floats. The 'if' selects for floats to go to that call while anything else would go to the simple call: .print(variable);

Again, that form of the .print call never sees a String, as my code is written, but the compiler still rejects it. How to work around that???

J-M-L:
try this, just typed here based on your code above, but should work.

Indeed it does. Thank you.

So now, I can reassign a value by overwriting the const char* variable, such as :

goodbye = "Hello Again";

And I tested that and it works.

(My brain was locked up thinking of const variables as not changeable, so I had not tried anything like that).

Again, many thanks for your help!

Read types from right to left:

char * reads as a pointer to a char. you can change the pointer or what is pointed.

const char * reads as a pointer to a char that is constant ==> here you can change the pointer but not the content.

char * const would be a constant pointer to a char ==> here you can change the char, not the pointer

const char * const would be a constant pointer to a char that is constant ==> here you can't change anything