Go Down

Topic: Variable argument count for print/println (Read 309 times) previous topic - next topic

fat16lib

Jul 04, 2018, 04:05 pm Last Edit: Jul 15, 2018, 06:32 pm by fat16lib
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:

Code: [Select]
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.

Code: [Select]
#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

Code: [Select]
#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.


pizzapie

Looks like something I will use. Is it in the Library Manager?

fat16lib

#2
Jul 05, 2018, 03:48 pm Last Edit: Jul 05, 2018, 03:48 pm by fat16lib
The entire source for VaPrint is in VaPrint.h.  You can just download the example and use the file.


If you want to use the file as a library, create a Arduino/libraries/VaPrint folder and place VaPrint.h in  the folder.

I may publish a library.  I am experimenting with features for field width with left and right adjusted fields.

fat16lib

#3
Jul 09, 2018, 03:54 pm Last Edit: Jul 09, 2018, 04:00 pm by fat16lib
I have been experimenting with left and right aligned fixed width fields.

This feature requires more memory and is not as efficient as unaligned fields.

Numerical fields must be formatted in an internal buffer to determine the amount of padding for right alignment.

I decided to add new functions printw() and printwln() that accept width and fill character arguments in addition to base and precision arguments.

This way variable argument versions of print()/println() are efficient and have low memory overhead.

I have change the behavior of format arguments so they only apply to the next data argument.
 
This example will print "FF,255".
Code: [Select]
  int h = 255;
  vp.print(Hex, h, ',', h);


Here is an example using printw()/printwln() to print aligned fields.

Code: [Select]
#include "VaPrint.h"

VaPrint vp(&Serial);

// Define a format that might be used multiple times.
// print uint16_t as six character hex string 0XHHHH.
#define Hex4 "0X",setw(4),setf('0'),Hex

void setup() {
  Serial.begin(9600);
 
  vp.printwln(setw(-6), "Hex", '|', setw(5), "Dec", '|');
   
  for (uint16_t i = 1; i; i *= 2) {
    vp.printwln(Hex4, i, '|', setw(5), i, '|');
   
    // Equivalent line without Hex4 macro.
    // vp.printwln("0X", setw(4), setf('0'), Hex, i, '|', setw(5), i, '|');     
  }
}
void loop() {}


Here is the output:
Code: [Select]
Hex   |  Dec|
0X0001|    1|
0X0002|    2|
0X0004|    4|
0X0008|    8|
0X0010|   16|
0X0020|   32|
0X0040|   64|
0X0080|  128|
0X0100|  256|
0X0200|  512|
0X0400| 1024|
0X0800| 2048|
0X1000| 4096|
0X2000| 8192|
0X4000|16384|
0X8000|32768|


I have defined four formatting argument functions. setb(base), setf(fillChar), setp(fpPrecission), and setw(fieldWidth).  Use positive fieldWidth for right alignment and negative fieldWidth for left alignment.

I have attached the current version of the VaPrint library there are two examples to demonstrate variable argument versions of print(), println(), printw(), and printwln().



Go Up