I have been experimenting with a wrapper for Print that implements a variadic version of print/println.
This started as an exercise to understand variadic templates but turned out to be very interesting.
Variadic templates can be used is to implement type-safe variadic functions. printf is not type-safe since the argument types are encoded in the format string.
Size is another advantage over a C style printf. Implementations of printf must link code for all types. The compiler can't determine needed code since the argument types is in the format string.
Variadic templates select only the needed code at compile time. So, if you don't use floating point, the code for double/float is not linked.
Here is an example sketch. The class VaPrint is a wrapper for Print.
Note that VaPrint use special arguments like Base(b) or Hex to specify the default base and Prec(p) to set floating point fraction precision.
The example prints the following:
2018-07-04 04:52:02
Error: 0X1,0X1C
current: 1.235 amps
Here is the example with code for standard Print and VaPrint. USE_VAPRINT selects the version to use.
#include "VaPrint.h"
// non-zero for variable arg count, zero for standard print.
#define USE_VAPRINT 1
uint16_t Y = 2018;
uint8_t M = 7;
uint8_t D = 4;
uint8_t hr = 4;
uint8_t mn = 52;
uint8_t sec = 2;
uint8_t code = 1;
uint8_t status = 0X1C;
float current = 1.2345;
// Helpers for ISO date time.
#define SD(n) (n>9?"-":"-0") // Dash separator.
#define SB(n) (n>9?" ":" 0") // Blank separator.
#define SC(n) (n>9?":":":0") // Colon separator.
void setup() {
Serial.begin(9600);
#if !USE_VAPRINT
// Print ISO date time with fixed width fields.
Serial.print(Y);
Serial.print(SD(M));
Serial.print(M);
Serial.print(SD(M));
Serial.print(D);
Serial.print(SB(hr));
Serial.print(hr);
Serial.print(SC(mn));
Serial.print(mn);
Serial.print(SC(sec));
Serial.println(sec);
Serial.print(F("Error: 0X"));
Serial.print(code, HEX);
Serial.print(F(",0X"));
Serial.println(status, HEX);
Serial.print("current: ");
Serial.print(current, 3);
Serial.println(" amps");
#else // USE_VAPRINT
VaPrint vp(&Serial);
// Print ISO date time with fixed width fields.
vp.println(Y, SD(M), M, SD(D), D, SB(hr), hr, SC(mn), mn, SC(sec), sec);
// Use Hex to print code and status then restore default Dec base.
vp.println(F("Error: 0X"), Hex, code, F(",0X"), status, Dec);
// Use Prec(3) for three digit fraction then Prec() to restore default.
vp.println("current: ", Prec(3), current, " amps", Prec());
#endif // USE_VAPRINT
}
void loop() {}
Standard Print uses 18 print/println calls VaPrint uses three println calls. There is a size penalty for VaPrint but it is small.
Uno:
VaPrint 3076 bytes.
Standard Print 2928 bytes.
Zero:
VaPrint 18896 bytes.
Standard Print 18848 bytes.
Here is VaPrint.h
#ifndef VaPrint_h
#define VaPrint_h
struct Base {
Base(uint8_t b = 10) : base(b) {}
uint8_t base;
};
const Base Bin(2);
const Base Oct(8);
const Base Dec(10);
const Base Hex(16);
struct Prec {
Prec(uint8_t p = 2) : prec(p) {}
uint8_t prec;
};
class VaPrint {
public:
VaPrint(Print* pr) : m_pr(pr), m_base(10), m_prec(2) {}
void print(Base b) {m_base = b.base;}
void print(Prec p) {m_prec = p.prec;}
void print(String s) {m_pr->print(s);}
void print(char c) {m_pr->print(c);}
void print(char* str) {m_pr->print(str);}
void print(const char* str) {m_pr->print(str);}
void print(const __FlashStringHelper *str) {m_pr->print(str);}
void print(float f) {print((double)f);}
void print(double d) {m_pr->print(d, m_prec);}
template<typename T> void print(T arg) {
m_pr->print(arg, m_base);
}
template<typename T, typename... Rest>
void print(T value, Rest... rest) {
print(value);
print(rest...);
}
void println() {m_pr->println();}
template<typename T, typename... Rest>
void println(T value, Rest... rest) {
print(value);
println(rest...);
}
private:
Print* m_pr;
uint8_t m_base;
uint8_t m_prec;
};
#endif // VaPrint_h
I have attached the example.
15 Jul 2018, Please see the updated library and examples below.