Simple Macro program I think

I'm trying to write a macro that i can best explain by the following sudocode.

#define printMany(arg1, arg2, arg3...etc) Serial.print(arg1); Serial.print(arg2); Serial.print(arg3); etc.

The args can be any type, int, char, float, etc. Hopefully the the qty of args can vary and all will be printed.

Basically I want to simplify the coding to print multiple items.

I'm not terribly fluent in Arduino programing but I have written simple macros in the past.

Thanks in advance
Frank

could use sprintf. values printed in nicely lined up columns

        sprintf (s, " Signal: %2d %2d %2d %2d - %2d %6d",
               RedPin, Yellow1Pin, GreenPin, Yellow2Pin, readPin, ambientValue);
        Serial.println (s);

Thanks for the suggestion, but my actual problem is more complicated than my simple example. Also I'm guessing that using sprintf requires a lot of thinking for each invocation. With my sudocode you don't have to identify the type of arg, etc. Serial.print doesn't care.

Actually I'm not using Serial.print rather I have an overload function that I'll be calling.
But explaining that makes things a lot more complicated. I'm pretty sure a macro is a good solution if it has the capabilities implied by my sudocode.
Frank

Macros should be avoided unless absolutely necessary, consider a template:

void printMany() {
}
template <typename T, typename ... Remainder>
void  printMany(T first, Remainder ... remainder) {
  Serial.println(first);
  printMany(remainder...);
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  float f {3.14};
  uint8_t u16 {10};
  int16_t i16 { -5001};
  const char str[] {"Hello World"};

  printMany(f, u16, i16, str);
}

void loop() {
}

Output:

3.14
10
-5001
Hello World

I don't think that that is possible to do with C preprocessor macros.

I don't have muchknowledge about macros.

Somewhere years ago I found a similar macro like this online and then modified it to my needs.

one important thing to know is that the backslash at the end of the lines has a very special meaning and is required as a must.

inside a "#define"-macro this backskash "\" has the meaning

"macro continues in the next line.

If you delete a single backslash the macro ends at the line above and you well get strange sounding compiler-errors that only make sense if you are know this funcionality of the backslash inside a "#define-macro"

If you look inside these macros you will see that some of them use the integertype long

or

float

A macro does replace source-code before
compiling. And this is the reason why you can't mix integer and float types directly.

If you want to do that you will have to write a macro that is based on the technique that user @gfvalvo showed in his demo-sketch.

// MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START *
// a detailed explanation how these macros work is given in this tutorial
// https://forum.arduino.cc/t/comfortable-serial-debug-output-short-to-write-fixed-text-name-and-content-of-any-variable-code-example/888298

#define dbg(myFixedText, variableName) \
  Serial.print( F(#myFixedText " "  #variableName"=") ); \
  Serial.println(variableName);

#define dbgi(myFixedText, variableName,timeInterval) \
  { \
    static unsigned long intervalStartTime; \
    if ( millis() - intervalStartTime >= timeInterval ){ \
      intervalStartTime = millis(); \
      Serial.print( F(#myFixedText " "  #variableName"=") ); \
      Serial.println(variableName); \
    } \
  }

#define dbgc(myFixedText, variableName) \
  { \
    static long lastState; \
    if ( lastState != variableName ){ \
      Serial.print( F(#myFixedText " "  #variableName" changed from ") ); \
      Serial.print(lastState); \
      Serial.print( F(" to ") ); \
      Serial.println(variableName); \
      lastState = variableName; \
    } \
  }

#define dbgcf(myFixedText, variableName) \
  { \
    static float lastState; \
    if ( lastState != variableName ){ \
      Serial.print( F(#myFixedText " "  #variableName" changed from ") ); \
      Serial.print(lastState); \
      Serial.print( F(" to ") ); \
      Serial.println(variableName); \
      lastState = variableName; \
    } \
  }
// MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END *

unsigned long MyTestTimer;

byte myByte = 0;
int myInt = -100;
long myLong = -1000000;
float myFloat = -12345.78;


void setup() {
  Serial.begin(115200);
  Serial.println("Setup-Start dbg-Macro-Demo");
  PrintFileNameDateTime();
  MyTestTimer = millis();
}


void loop() {
  myInt++;
  dbgi("02",myInt,2000); 

  myLong += 100;
  dbgi("03",myLong,4000);
    
  if ( TimePeriodIsOver(MyTestTimer,1000) ) {
    myByte++;
    dbg("00",myByte);
    
    myFloat += 2345.67;
  }  
  dbgcf("999",myFloat);
}



// helper-functions
void PrintFileNameDateTime() {
  Serial.println( F("Code running comes from file ") );
  Serial.println( F(__FILE__) );
  Serial.print( F("  compiled ") );
  Serial.print( F(__DATE__) );
  Serial.print( F(" ") );
  Serial.println( F(__TIME__) );  
}


// easy to use helper-function for non-blocking timing
// explanation see here
// https://forum.arduino.cc/t/example-code-for-timing-based-on-millis-easier-to-understand-through-the-use-of-example-numbers-avoiding-delay/974017
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - startOfPeriod >= TimePeriod ) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else return false;            // actual TimePeriod is NOT yet over
}

void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {
  static unsigned long MyBlinkTimer;
  pinMode(IO_Pin, OUTPUT);
  
  if ( TimePeriodIsOver(MyBlinkTimer,BlinkPeriod) ) {
    digitalWrite(IO_Pin,!digitalRead(IO_Pin) ); 
  }
}

I don't know anything about how to use templates like user @gfvalvo showed in his demo-sketch.

To me the given names are not all clear.

@gfvalvo

"first" somehow indicate the first, first what?
first type or first variable?

Is the meaning of the three dots "..."
there can follow a list of whatever kind variables?

Why did you name "T" T?

I think there can be much more selfexplaining typenames and variable names.

And how the heck does this work to have the exact same function name

why do you name "Remainder" Remainder?
And why is it written one time with a capital letter Remainer
and one time with a lower-case letter?

Like many languages, C++ has a way of defining functions with a variable number of arguments, zero (not one) or more ("variadic functions")

C++ supports function overloading: creating variant functions with the same name, but with arguments that vary in number and/or type. println is a very common example; it takes over a dozen different kinds of arguments, including no arguments.

It's common practice in several languages (with at least one notable exception) to make type names uppercase, and instances of those lowercase, e.g. Button button. It's also common in the case of generic functions and/or templates to use a single uppercase letter, since it's a placeholder: T for a single type; K for key type and V for value type, etc

T first works like any other C++ argument declaration: an argument named first of type T. This overload therefore takes one or more arguments. Only the type of the first argument "matters" to call println.

Then the remainder of placeholder type Remainder is expanded, which calls another instance of printMany (just plain recursion) -- unless that list of arguments is empty: the template requires at least one argument. However, there is separately a printMany defined that takes no arguments, which happens to do nothing.

seems that you want a macros with an arbitrary # of arguments. google variadic functions

Added: @kenb4 thanks, scrolled right past that and would swear it wasn't there first time through…

Obviously. I know nearly as little, but the contribution of @gfvalvo makes perfect sense, even if I couldn't roll something like that myself.

Please do not try to figure this out in a forum exchange. Go away and learn about this elsewhere, maybe that course you are always promoting gets there.

There are three identifiers of any of any significance.

printMany. function self explains itself, as you might put it.

first. the first element of the list of things to print

remainder. the rest of the list.

And that call to an identically named function is… recursion. Do you know about that?

i'd switch it to not use println and let the caller put her own line breaks where she wants.

a7

Not quite. The template is expanded recursively, but the function is not called recursively (in the sense of a function calling itself). Rather, the complier generates a flat series of calls to overloaded functions. It looks like this in concept:

void printMany() {
}
void printMany(const char arg3[]) {
  Serial.println(arg3);
  printMany();
}
void printMany(int16_t arg2, const char arg3[]) {
  Serial.println(arg2);
  printMany(arg3);
}
void printMany(uint8_t arg1, int16_t arg2, const char arg3[]) {
  Serial.println(arg1);
  printMany(arg2, arg3);
}
void printMany(float arg0, uint8_t arg1, int16_t arg2, const char arg3[]) {
  Serial.println(arg0);
  printMany(arg1, arg2, arg3);
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  float f {3.14};
  uint8_t u16 {10};
  int16_t i16 { -5001};
  const char str[] {"Hello World"};

  printMany(f, u16, i16, str);
}

void loop() {
}

I did not post that series of macro definitions, you must be thinking of another forum member.

Wow, Thank you, great information by all.
Special thanks to those who took the time to submit such detailed and lengthy explanations.
It's going to take me some time to digest everything and I will study every response. I'm not really Audrino knowledgeable and in my 80's so it will take a while. I'll post back with my results.
Again, thanks to all
Frank

Just an update on my progress, which is minimal.
Some of the suggestions here were over my head and I haven't been able to take advantage of them.
Although I did find this solution:
#define printMany(arg1,arg2) Serial.print(arg1); Serial.println(arg2); etc, etc, ....

The basic disadvantage is that it only operates with the exact number of arguments specified in the macro. So I would need a separate macro for 1 arg, 2args, 3 args, 4 args, etc.

Having multiple such macros is doable and is certainly better than any other alternative that I have within my moderate knowledge base and capability.

If there are any suggestions on how to enable a variable number of arguments with this technique I would love to hear them.

In any case, thanks to all for the help and suggestions.
Frank

See My Post #4 Above. It will work for any number of arguments and for any datatype that Serial knows how to print.

With a minor adjustment, @gfvalvo 's template will print everything on one line

void printMany() {
  Serial.println(); // blank line after other arguments (if any)
}
template <typename T, typename ... Remainder>
void printMany(T first, Remainder ... remainder) {
  Serial.print(first);
  Serial.print(' '); // comment out if desired
  printMany(remainder...);
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  float f {3.14};
  uint8_t u16 {10};
  int16_t i16 { -5001};
  const char str[] {"Hello World"};

  printMany(f, u16, i16, str);
  printMany(f, u16, i16);
  printMany(f, u16);
  printMany(f);
  printMany();
  Serial.println("done");
}

void loop() {
}

prints

3.14 10 -5001 Hello World 
3.14 10 -5001 
3.14 10 
3.14 

done

Nothing simple unfortunately. Although macros can have a variable number of arguments, there isn't a way to iterate through them or a simple way to expand the macros recursively.

I did try the technique described at Recursive macros with C++20 __VA_OPT__ but was unable to get it to work, it also took an abnormally long time to compile.

I would suggest the "print2" "print3" etc macros are simple and effective. Once you have generated the macros, put them in a header file to keep them out the way.

gfvalvo, kenb4, (and others that may have contributed),
I'm sorry but this template thing is over my head. I've googled the topic, but I haven't "got it" yet. Worse if I did get it and had a problem down the road, I probably couldn't fix it.
I feel bad that you folks have taken the time to help and I can't use it. I take solace that down the road others smarter than me will enjoy your solution and therefore your time will not be wasted.

Thank you
Frank

So you simply come back to the forum to ask.

So why not asking questions about it to get more explanation?

@frank2644

you asked for a very versatile thing: print a varying number of parameters of varying type.
What do you expect? The more complex the functionality the more easy the solution?

Debugging buggy code requires time.

  • investing time in understanding more complex code
  • investing time into develop your code in small steps: add a few lines - test - repeat
  • investing time in adding a lot of different serial.print-statements
  • investing time into work to earn the money to pay an expert that will do it for you

Okay, I'll ask a question about templates.

How do I start? When I pasted the template into my code I got a lot of complier errors starting with:
"error: variable or field 'printMany' declared void"

That error was for this line of code:

void printMany(T first, Remainder ... remainder) {

Do I have to import the template in some way?