Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add variadic print(String...) function #58

Open
NicoHood opened this issue Jul 18, 2015 · 25 comments · May be fixed by #30
Open

Add variadic print(String...) function #58

NicoHood opened this issue Jul 18, 2015 · 25 comments · May be fixed by #30
Assignees

Comments

@NicoHood
Copy link

Just found this:
http://stackoverflow.com/questions/10044449/a-function-with-variable-number-of-arguments-with-known-types-the-c11-way

Add this to Print.h:

    template <typename ...TailType>
    size_t println(const String &head, TailType&&... tail)
    {
        size_t r = 0;
        r+=println(head);
        r+=println((tail)...);
        return r;
    }

Now you can do stuff like this:
Serial.println("Hey", "this", "is", "a", "Test");

Every word is printed in a new line. You can also add this for Serial.print (without newline).
Maybe this can be optimized to only print the last input with a new line. An overload with two arguments is needed here I guess.

Not sure if its perfect c++, but this would give us the option to print infinite strings at once.
Maybe one can generalize this even more to also print numbers (without formating of course, just DEC).

It takes a lot of flash though. But maybe someone can optimize this further? (Now that we also have c++11 with the merged PR)

Any comments on that?

Edit: Maybe this is also relevant. This uses the same size as if there was no template.
It can also handle numbers (without format).
https://msdn.microsoft.com/en-us/library/dn439779.aspx

    template <typename First, typename... Rest> 
    inline size_t println(const First& first, const Rest&... rest) {
        size_t r = 0;
        r+=println(first);
        r+=println(rest...); // recursive call using pack expansion syntax
        return r;
    }

Another example (ln now only adds a new line at the end):

    template <typename First> 
    inline size_t println2(const First& first) {
        return println(first);
    }

    template <typename First, typename... Rest> 
    inline size_t println2(const First& first, const Rest&... rest) {
        size_t r = 0;
        r+=print(first);
        r+=println2(rest...); // recursive call using pack expansion syntax
        return r;
    }
@NicoHood
Copy link
Author

Seems I got it working pretty good:

    // Addition by NicoHood
    template <typename First, typename Second, typename Third, typename... Rest> 
    inline size_t print(const First& first, const Second& second, const Third& third, const Rest&... rest) {
        // A Wrapper is needed here in order to not call the
        // Formatted functions if the input was three integers
        // If you input only two integers the formatted
        // Functions have higher priority
        return printWrapper(first, second, third, rest...);
    }

    template <typename First> 
    inline size_t printWrapper(const First& first) {
        return print(first);
    }

    template <typename First, typename... Rest> 
    inline size_t printWrapper(const First& first, const Rest&... rest) {
        size_t r = 0;
        r+=print(first);
        r+=printWrapper(rest...); // recursive call using pack expansion syntax
        return r;
    }

    template <typename First, typename Second, typename Third, typename... Rest> 
    inline size_t println(const First& first, const Second& second, const Third& third, const Rest&... rest) {
        // A Wrapper is needed here in order to not call the
        // Formatted functions if the input was three integers
        // If you input only two integers the formatted
        // Functions have higher priority
        return printlnWrapper(first, second, third, rest...);
    }

    template <typename First> 
    inline size_t printlnWrapper(const First& first) {
        return println(first);
    }

    template <typename First, typename... Rest> 
    inline size_t printlnWrapper(const First& first, const Rest&... rest) {
        size_t r = 0;
        r+=print(first);
        r+=printlnWrapper(rest...); // recursive call using pack expansion syntax
        return r;
    }

Code to test:

  Serial.println("1",  "2", 6, "3"," Word " , "4", '!', "5");
  Serial.println();
  Serial.println(44, 44, 44);
  Serial.println();
  Serial.println(44, HEX);
  Serial.println();

// this produces the same flash size:
  // Serial.println("1", "2", "3", "4", "5");
  Serial.println("1");
  Serial.println("2");
  Serial.println("3");
  Serial.println("4");
  Serial.println("5");

Edit: One could also adapt this to other functions like:
pinMode(2,6,3,9,INPUT);

Edit2: Just a thought: Maybe we can also change the DEC, HEX, BIN into an enum and then be able to print 2 integers and a formatted integer at the same time. Maybe, not tested.

@Chris--A
Copy link

At first glance, this seems like a great addition and I'm sure there are some further improvements we can look at. However this code will cause the current API to become C++11 only. Even if the code is wrapped in define guards, the reference will become separated unless a C++98 compatible version is provided using variadic functions.

@Chris--A
Copy link

I do have an idea though! I give it a test when I'm home in a few hours.

@q2dg
Copy link

q2dg commented Jul 19, 2015

Ooh, yeahh!! +1!!

@NicoHood
Copy link
Author

For some reason the print (without ln) has no higher priority on 1-2 function parameters. Meaning it will result in a loop and don't print anything. That is not a problem though, we can force the function to take at least 3 arguments to it wont bother at all. So if you input more than 2 args (>=3) it will print them in a row. No matter what type it is (int, float, String, char).

Formatted printing will not work I think.

I updated the code above. Should I create a PR to test this code?

Edit: @Chris--A Since we now have c++11, why not use it? I see no problem with that. Its enabled anyways.

@NicoHood
Copy link
Author

With the current pinMode layout it does not work with some overhead. At least I wasnt able to make it smaller. The problem is that the last argument is important for the other inputs. Here is a working, but more expensive example:

#include "Arduino.h"

// Addition by NicoHood
template <typename First, typename Second>
inline uint8_t pinModeWrapper(const First& first, const Second& second) {
  pinModeWrapper(first, second);
  return second;
}

template <typename First, typename... Rest>
inline uint8_t pinModeWrapper(const First& first, const Rest&... rest) {
  uint8_t mode = pinModeWrapper(rest...);
  pinModeWrapper(first, mode);
  return mode;
}

template <typename First, typename Second, typename Third, typename... Rest>
inline void pinMode(const First& first, const Second& second, const Third& third, const Rest&... rest) {
  uint8_t mode = pinModeWrapper(second, third, rest...);
  pinMode(first, mode);
}

@Chris--A
Copy link

I agree it should be used, however as it hasn't been released yet (an IDE with C++11 enabled) making the API version incompatible is quite restrictive. This should be discussed first I think (with the users, not dev team).

Many people chose to use the older IDE when breaking changes for IDE 1.0 were released, and subsequently many libraries written after 1.0 contained checks for WProgram.h to cater for the many people that used out dated third-party forks, or simply didn't want to upgrade.

Adding in this breaking change will prevent people from testing libraries/code for the older IDE's (by only a few months) and give many people further reason to not upgrade. As this code is just a convenience and doesn't allow a result that wasn't already possible, careful consideration should be put into this addition.

@NicoHood
Copy link
Author

Hum? C++11 is in the current 1.6.6 release merged.

This is a minor feature, nothing so much new. Its just a nice addition for the 1.6.x branch. Doesnt mean you cannot stick to the old version. And that there is a gap between 1.0.x and 1.6.x we already know and it will grow from update to update. That is progress in development. And this tiny addition shouldnt keep us from adding it.

They changed the whole USB core and I think this wasnt backported to 1.0.x, if they ever release 1.0.7. So the whole USB code will be more or less incompatible without some fixes. So I dont get the problem of adding this.

@Chris--A
Copy link

The current release tag is 1.6.5, 1.6.6 hasn't been released. As minor as it is, if I enable gnu++98, the API no longer is compatible. It doesn't mean you can't use it, it means you cannot even compile the code.

At the very minimum, like the other C++11 code already implemented in the API, it should be disabled for C++98.

It should also be noted that many people can't use the latest releases due to their OS or other restrictions not supporting it (Mac has a minimum version now if I recall).

But either way, the code will eventually have to look at moving forward and forgetting the past, it just needs to be done carefully.

@NicoHood
Copy link
Author

I am working with the nightly build. And this has c++11 and 1.6.6 will also have c++11

@Chris--A
Copy link

Yes, however this is not the point I'm arguing.

@shiftleftplusone
Copy link

no idea if this helps, but:
for adding (concatenating) ANSI strings I'm using my own stradd() funktion:

char * stradd(char * s, int n, ...)  // "adds strings" s=sumstring , n=number_in_list, ... = var string list
{
    va_list vlst;   
    int i;

    char * bufptr;
    char * retptr = s;

    va_start(vlst, n);
    for (i=1; i<=n; ++i)   {
       bufptr = va_arg(vlst, char *);
       strcat(s, bufptr);
    }
    va_end(vlst);   
    return retptr;   
}

Having this, you can concat a variable number of strings (tokens) to 1 long string and then do with the complete resulting string whatever you want.

OT:
the editor for posting (e.g., source code) is really a crap!

@matthijskooijman
Copy link
Collaborator

@Chris--A, wrapping these methods in proper #ifdefs would solve the "it doesn't compile", I guess. But I'm not sure why this would be needed? Is there any good reason to disable C++11 mode for the Arduino core? There are probably other compiler flags that, if you change them, will break the compilation?

Another point that has not been mentioned is conflicts with the current print() methods. In particular the integer and float printing versions:

 Serial.print(100, 16);

Should this print "10016", or "64"?

It seems the conflict is minor, but I'm not sure which version will be favored by the compiler, and it is fundamental - if you have an any-number-of-arguments version, you can't really use extra arguments for specifying the formats.

the editor for posting (e.g., source code) is really a crap!

It helps if you use markdown for your code blocks. I added ``` around it to make it a proper code block now.

@NicoHood
Copy link
Author

The current template above only works for >=3 parameters and wont do integer formating.
Meaning print(100, 16, 42); will give "1001642"

Otherwise we'd have to use enums for the HEX and DEC definitions and exclude the enums in the template (and I dont know how to do that or if its possible). Thatswhy the function is kept for >=3 args which should not conflict.

@matthijskooijman
Copy link
Collaborator

I'm afraid that if print("Hello", " ", "World") works, but print("Hello", "World") does not, people will be terribly confused and this feature hurts more than it helps.

Using enums for HEX etc. could help, but that won't help for the print(float, precision) version, where the precision is specified as a plain int instead of a constant...

@NicoHood
Copy link
Author

What we could do is:

  • Keep the 3 arg template above
  • Add 2 arg wrappers for any other possibility (with enums for formatting)

@stickbreaker
Copy link

Why not sidestep the print() collision issue by adding this variadic but naming it printx().

Chuck

@NicoHood
Copy link
Author

Good Idea! Or maybe we also add something like printAnything?
https://github.com/arduino/Arduino/issues/3691

@shiftleftplusone
Copy link

better then implement a variadic "real" printf() subsidiary instead, feat. then also
Serial.printf("%s %s %s %d !", "Hello", "World", "in the year", 2015)

That actually would finally make the Sketch print command more ANSI C stdio.h compatible !

@Chris--A
Copy link

I'm quite fond of this idea, and have added a similar function to my PrintEx library. It is based on my streaming implementation and can therefore use all its manipulators.

The code produced ends up smaller than the equivalent written using multiple print()/println() calls.

An example usage:

  PrintEx printer = Serial;
  printer.printx( "A hexidecimal number: ", hex, 4398 );
  printer.printx( "A floating point number: ", precision(4), 12.3456f );

@shiftleftplusone
Copy link

yes, I also found your lib, it's excellent, especially your printf() implementation!
Gladly looking forward to your additional implementation of width.precision specifiers (e.g., "%6.2f" )!

@Chris--A
Copy link

@vogonjeltz
Thank you for your kind words.

Trying not to send this issue spiraling off topic, however just thought I'd let you know, as of version 1.1.5, the precision parameter is now available (lib manager should update within the hour).

A test:

#include <PrintEx.h>

PrintEx serial = Serial;

void setup() {

  Serial.begin(9600);
  serial.printf( "A floating point test: %9.7f\n", 1.23456f );
  serial.printf( "A floating point test: %9.6f\n", 1.23456f );
  serial.printf( "A floating point test: %9.5f\n", 1.23456f );
}

void loop() {}

Result:

A floating point test: 1.2345600
A floating point test:  1.234560
A floating point test:   1.23456

@shiftleftplusone
Copy link

+1
8-)

@cousteaulecommandant
Copy link

What happened to this idea? Is requiring C++11 still a turn-off?

I was thinking on a similar approach, exploiting inlining (I haven't tried it yet though). This is how my Print.h ended up looking:

#define INLINE __attribute__ ((__always_inline__)) // undefined at the end
/* ... */
    size_t print(const __FlashStringHelper *);
    size_t print(const String &);
    size_t print(const char[]);
    size_t print(char); // NB: char != signed/unsigned char
    size_t print(signed long, int = DEC); // NB: signed long == long
    size_t print(unsigned long, int = DEC);
    size_t print(double, int = 2);
    size_t print(const Printable&);
    INLINE size_t print(unsigned char  n, int b = DEC) { return print((unsigned long) n, b); }
    INLINE size_t print(unsigned short n, int b = DEC) { return print((unsigned long) n, b); }
    INLINE size_t print(unsigned int   n, int b = DEC) { return print((unsigned long) n, b); }
    INLINE size_t print(signed char  n, int b = DEC) { return print((signed long) n, b); }
    INLINE size_t print(signed short n, int b = DEC) { return print((signed long) n, b); }
    INLINE size_t print(signed int   n, int b = DEC) { return print((signed long) n, b); }
    
    size_t println(void);
    template<typename... Ts>
    INLINE println(Ts... args) {
      size_t sz = print(args...);
      sz += println();
      return sz;
    }
    
    // Variadic print[ln] (for 2 or more args)
    template<typename T, typename T2, typename... Ts>
    INLINE print(T arg, T2 arg2, Ts... args) {
      size_t sz = print(arg);
      sz += print(arg2, args...);
      return sz;
    }
    // Use this template with 3 or more args when the 2nd arg is an int
    template<typename T, typename T2, typename... Ts>
    INLINE print(T arg, int b, T2 arg2, Ts... args) {
      size_t sz = print(arg, b); // will choose non-templated over templated
      sz += print(arg2, args...);
      return sz;
    } //!!TODO!!// check if this works
/* ... */
#undef INLINE

Properties of this code:

  • I haven't tested it yet.
  • Adds variadic print, of course.
  • It simplifies Print.cpp rather than complicating it; I think the simpler the things, the harder to break.
  • This implementation is (or should be) compatible with the current print(int,int) method: doing print(255, 16, 42) will print FF42, not 2551642. If you want to concatenate integers (why would you want that, though?) use something like print(255, "", 16, "", 42) or multiple calls. (In any case, I don't like this way to print integers, but I wanted this to not break old code/documentation.)
  • println has been replaced with a variadic template. This is mainly helpful for future development: this way, changes done on a print function don't need to be replicated on the corresponding println. Additionally this seems to save around a dozen bytes for some examples I compiled.
  • println adds a single end of line, not one per argument. I think this makes more sense, as in "print a line that says blah, blah, blah".
  • Rather than using the inline keyword, which proved ineffective in some cases (no idea why), I just used GCC's __attribute__ ((__always_inline__)). This made variadic print use exactly the same amount of memory as multiple prints.
  • I used (and maybe abused?) inline definitions for the [un]signed char, [un]signed short, and [un]signed int versions of print. This will potentially reduce the program size for example when both print(int) and print(long) are used, plus the actual print(long) function might be called slightly faster. (Also this adds a print method for short, and simplifies the code in Print.cpp which is always a good thing.)
  • Using signed long rather than just long, just for clarity (both are equivalent but this way it's clearer that there are signed and unsigned versions of each).

In short, I think it's an elegant solution as it simplifies the code, it's backwards compatible with old code, and doesn't have a negative impact on performance nor resource usage.

If C++11 is still a problem, I guess println could be left as is, and wrap all the variadic template thing inside an #if __cplusplus <= 201103L. (In this case, a variadic version of println should be made as well.)

@cousteaulecommandant
Copy link

By the way, and I think this is the most important point: C++ has rules for resolving overloaded functions. So if you do print(long, int) it'll choose the print(long, int) form over the template<typename T1, typename T2> print(T1, T2) form. So I think C++ got us covered here.

http://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function

In any case, my previous code was made in a "lazy" and "timid" way, making as few changes as possible and keeping everything as backwards compatible as possible, hoping that that'll promote its adoption. It is true that using an enum or a special class for the base (so that you could use HEX, OCT, or BASE(36)) and maybe for the precision (PRECISION(2)) would be more elegant in general, but I think the fewer things that are changed, the better. This can always be implemented later.

@sandeepmistry sandeepmistry transferred this issue from arduino/Arduino Sep 16, 2019
@per1234 per1234 linked a pull request Feb 4, 2023 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants