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

Variadic print #5829

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

cousteaulecommandant
Copy link
Contributor

Allows combining multiple arguments into a single call to Print::print[ln], like Serial.print("Answer=", 66, HEX).
This feature requires C++11 or newer to work, but if it is not available the old calls can still be used.
This change is backwards compatible: print(42, 16) prints "2A" as always, not "4216" (the latter can still be achieved with print(42, "", 16) ).
Many functions from Print.cpp have been removed and replaced by inline versions in Print.h, most notably the println(...) ones. This results in a simplified (and thus easier to maintain) Print.cpp and slightly smaller executables.

Copy link
Collaborator

@matthijskooijman matthijskooijman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, these changes look good. I've only looked at the AVR changes, I assume the SAM changes are identical?

Looking at Print.cpp now, I think that it would be beneficial to also move them other single-line print() methods to Print.h and make them always_inline (or perhaps just inline). Or is there any reason not to?

What I am wondering is how LTO plays into this. IIRC we have that enabled by default now, so you would think the compiler would have been able to do this inlining already. Perhaps it decides not to, so only the always_inline attributes are needed. Would you care to try adding just the attribute, without moving the definitions from Print.cpp to Print.h and see if that works?

Regarding the commit structure: You have this weird PR merging commit included, I suspect you did something weird to update your fork of the repo or something? That commit should go.

The actual changes are all lumped together in the second commit, but I think there are a few logical changes here that should ideally be in separate commits. I think it's three changes: Replacing println with a template, adding always_inline & moving definitions, and adding varadic versions.

@@ -31,6 +31,8 @@
#define OCT 8
#define BIN 2

#define INLINE __attribute__ ((__always_inline__)) inline // undefined at end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be called PRINT_INLINE or something else more explicit (ARDUINO_INLINE?), since INLINE is more_likely to conflict with other header files. The fact that you undef it later helps, but not if another definition of INLINE is included before Print.h.

I also believe the inline keyword should not be needed here, since these are all methods. Doesn't really hurt either, though.


/** Variadic methods **/
#if __cplusplus >= 201103L
// Ensure there are at least two parameters to avoid infinite recursion.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deserves a bit more explanation, best to include an example.

Remove all definitions of println(...) from Print.cpp except println(void),
and replace them with inline methods in Print.h.
This simplifies the code in Print.cpp improving maintainability.
Remove print(char | char[] | Printable | unsigned char | int | unsigned int)
from Print.cpp and replace them with inline versions in Print.h.
Add explicit print() support for signed char, unsigned/signed short, and float.
Also, add `signed` keyword explicitly to int and long (even if unnecessary).

Notice that `char` and `signed char` are considered different types,
even if their ranges are the same (ditto for `unsigned char` in SAM).
Both AVR and SAM seem to define [u]int8_t explicitly as [un]signed char
and not as char, so printing them will print them as numbers as expected.
(This does not apply to `int` and `signed int`; those are the same type.)
Add GCC __attribute__ ((__always_inline__)) to inlined methods in Print.h.
This seems necessary since not doing so increases the executable size
(probably because that would create several function definitions),
even with LTO optimizations.
This change combined with the inlining of print/printf methods seems to reduce
the executable size for most applications by a fair amount.
(Note that the `inline` keyword does not imply actual inlining.)
Allows combining multiple arguments into a single call to Print::print[ln], like Serial.print("Answer=", 66, HEX).
This feature requires C++11 or newer to work, but if it is not available the old calls can still be used (the change has been wrapped in `#if __cplusplus >= 201103L ... #endif`).
This change is backwards compatible: print(42, 16) prints "2A" as always, not "4216" (the latter can still be achieved with print(42, "", 16) ).  This also works with multiple arguments: any numeric argument followed by an int will be treated as a (number, format) pair.
The variadic function calls are inlined using an __attribute__ ((__always_inline__)), so they're equivalent to writing multiple sequential calls to print with one argument.
@cousteaulecommandant
Copy link
Contributor Author

Everything done, thanks for the feedback!

As requested via IRC, here's an example sketch for testing backwards compatibility:
https://gist.github.com/cousteaulecommandant/3083c33cc83497d05e1fa12ae8cfaf17#file-example_backwards_compat-ino

As can be seen, the resulting size is slightly smaller with the new code. However, some examples show a huge improvement, such as this one, where using inlined functions implies creating only 3 functions and not 4:
https://gist.github.com/cousteaulecommandant/3083c33cc83497d05e1fa12ae8cfaf17#file-example_println_inlining-ino
On this other example, however, the resulting code is a few bytes larger, since it has to include several extra calls to println(void) that were formerly included in the previous call:
https://gist.github.com/cousteaulecommandant/3083c33cc83497d05e1fa12ae8cfaf17#file-example_println_inlining_2-ino

Regarding documentation, the only feature that has been added is the variadic support (the other commits inside this PR are just optimizations or improvements on readability and maintainability), so there shouldn't be big changes in the documentation. In any case, the current documentation is still valid since these changes were designed to be backwards compatible.

@matthijskooijman
Copy link
Collaborator

@cousteaulecommandant, I finally got around to having another look at this. As we discussed on IRC, I'd like to see how feasible it is to extend this to also support custom formatters and/or more formatting options. As a proof-of-concept I made matthijskooijman@a3f0e33 today, which allows passing custom "formatter" objects after a value to format (e.g. instead of HEX). I've found that to reliably do this, I needed to move the actual printing code into a (non-variadic) doPrint() method, so print() can just take care of the variadic arguments (using some template magic in check_type, in a way similar to std::enable_if, to decide whether a second argument is a formatter or just something to print). As an added advantage, it seems it is no longer needed to repeat the list of int-as-second-argument-for-base-formatting as variadic versions). It might make sense to move this change into your PR if it really ends up being needed.

I'm still not sure how to add formatting options (e.g. options to default or other formatters) and especially how to combine these, so that needs a bit more thought.

@cousteaulecommandant
Copy link
Contributor Author

Personally I don't think we should be putting too much effort into formatters for Print, since

  • Print is already quite complex,
  • something like Serial.print(Format(number, digits, base, etc)) would be easier to implement and understand, and (depending on how it's implemented) not only useful for Print but also String and other uses,
  • printf (Add function "printf" to AVR core class "print" #5938) might be a simpler solution for complex formatting (and if "%02X" is too scary for newbies, maybe a "printf formatter constructor class" could be developed).

In other words, if complex and extensible formatters end up being added, I'd go for functions that return a Printable or a String rather than arguments to Print::print, as they would be much easier to implement and document, and move the formatting problem somewhere else.

(Serial.print(number, HEX) et al would need to be kept for backwards compatibility, but I would avoid going in that direction for new formatting features.)

// Ensure there are at least two parameters to avoid infinite recursion.
// e.g. `StringSumHelper s; print(s)` may be treated as `print(s, ...)`
// with `...` being the empty list, thus calling `print(s)` again.
// (This is because print(StringSumHelper) isn't explicitly defined.)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with this - with one parameter, won't this simply fall back on print(), which you could define privately as a no-op?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recall that this was tried too, but that triggered other corner cases (that I can't recall right now). With care, it can be made to work though (I have some local changes on top of this PR that do so, as well as add some formatting support, but I can't get around to finishing them...).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with one parameter, won't this simply fall back on print(), which you could define privately as a no-op?

It will fall back on print(s) followed by print(). The latter could indeed be implemented as a no-op, but the former will be expanded to print(s) followed by print() again, entering an infinite loop (at compile time).
I don't remember the exact situation that caused this (I think it was something about passing a StringSumHelper but I cannot come up with an example right now); only that infinite recursion happened.

@CLAassistant
Copy link

CLAassistant commented Apr 9, 2021

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Print and Stream class The Arduino core library's Print and Stream classes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants