Reverse string building
Building strings containing mostly integers can simplify and speed up the operation significantly. For us humans, using an imperative language like C, that reverse order can be somewhat overwhelming.
For example, consider a function that is provided by a buffer with room for at least 25 chars, and three signed 16 bit integers, and formats the integers into human-readable three-component geometric vector, "(x, y, z)":
#include <stdint.h>
static char *backwards_i16(char *ptr, int16_t val)
{
uint16_t u = (val < 0) ? (uint16_t)(-val) : (uint16_t)(val);
do {
*(--ptr) = '0' + (u % 10);
u /= 10;
} while (u);
if (val < 0)
*(--ptr) = '-';
return ptr;
}
/** Return a pointer to "(x, y, z)" somewhere within the buffer,
* with buffer having room for at least 1+6+2+6+2+6+1+1 = 25 chars.
*/
char *i16_vec3_to_asciiz(char *buffer, int16_t x, int16_t y, int16_t z)
{
char *p = buffer + 1 /* '(' */
+ 6 /* -32768 .. 32767 */
+ 2 /* ", " */
+ 6 /* -32768 .. 32767 */
+ 2 /* ', " */
+ 6 /* -32768 .. 32767 */
+ 1 /* ')' */
+ 1 /* terminator, '\0'. */ ;
*(--p) = '\0';
*(--p) = ')';
p = backwards_i16(p, z);
*(--p) = ' ';
*(--p) = ',';
p = backwards_i16(p, y);
*(--p) = ' ';
*(--p) = ',';
p = backwards_i16(p, x);
*(--p) = '(';
/* Note: if the caller expects the result to start at the beginning
of the buffer, we need to do the equivalent of
if (p > buffer)
memmove(buffer, p, (size_t)(buffer+1+6+2+6+2+6+1+1 - p));
Our description says result is somewhere within the buffer,
so we do not need to.
*/
return p;
}
As you can see, the code is very compact, and relatively straightforward. What is difficult with it, is to remember that to see what kind of string it constructs one needs to read the code backwards: start at the Note: comment, then go upwards, until you see *(--p) = '\0'; which is responsible for terminating the string.
The basic operation used here is divide by ten with remainder. It does not imply that hardware division is actually used, though. We can write the basic operation as
unsigned char div10(unsigned int *const arg) {
const unsigned char result = (*arg) % 10;
(*arg) /= 10;
return result;
}
which on most architectures does not actually involve hardware division at all, but a (wider word-width) multiplication using a reciprocal represented by a fixed point integer. For example, on AMD64, Clang-10 -O2 generates the same code for above as for
unsigned char div10(unsigned int *const arg) {
const unsigned int dividend = *arg;
const unsigned int quotient = (uint64_t)((uint64_t)dividend * 3435973837) >> 35;
const unsigned char remainder = dividend - 10*quotient;
*arg = quotient;
return remainder;
}
where the magic constant, 3435973837 / 235 represents 0.1000000000.
In general, dividing an unsigned integer q with a constant non-power-of-two positive divisor d, are based on q/d ≃ (a×q+c)/2n, with the parenthesized part using extra range (often twice that of the quotient q).
Converting integers to strings using subtraction only
Sometimes you have an architecture where division by constant (implemented either in hardware, or similarily to above as multiplication via reciprocal) is too costly. There, you can use repeated subtractions.
For example, on and 8-bit architectures without hardware division or multibyte multiplication, you might find you need to efficiently convert 32-bit signed and unsigned integers to strings using subtraction only. Consider:
#include <stdint.h>
static const uint32_t powers_of_ten[9] = {
UINT32_C(10),
UINT32_C(100),
UINT32_C(1000),
UINT32_C(10000),
UINT32_C(100000),
UINT32_C(1000000),
UINT32_C(10000000),
UINT32_C(100000000),
UINT32_C(1000000000),
};
unsigned char u32_to_asciiz(char *const buffer, uint32_t value)
{
unsigned char pot = 0;
unsigned char len = 0;
while (value >= powers_of_ten[pot] && pot < 9) {
pot++;
}
/* Note: pot is the power of ten for the most significant decimal digit.
pot == 0 is equivalent to saying value < 10, and
pot == 9 is equivalent to saying value >= 1000000000. */
while (pot--) {
const uint32_t base = powers_of_ten[pot];
char digit = '0';
while (value >= base) {
value -= base;
digit ++;
}
buffer[len++] = digit;
}
buffer[len++] = '0' + value;
buffer[len ] = '\0';
return len;
}
unsigned char i32_to_asciiz(char *const buffer, int32_t value)
{
if (value >= 0) {
return u32_to_asciiz(buffer, (uint32_t)(value));
} else {
buffer[0] = '-';
return u32_to_asciiz(buffer + 1, (uint32_t)(-value)) + 1;
}
These require a buffer of sufficient size (12 chars will suffice for all possible values), and return the length, excluding the terminating nul byte. The string starts at the beginning of the buffer.
For example, compiling the above to ATmega32u4 using GCC 5.4.0 via avr-gcc -std=c11 -Os -Wall -ffreestanding -mmcu=atmega32u4 -c above.c, the above takes 202 bytes of ROM/Flash (166 bytes of code, 36 bytes for the constant array); 262 bytes with -O2 (226 bytes of code, 36 bytes for the constant array).
Interestingly, the runtime cost is not as nearly as big as one might think. The slowest 32-bit unsigned value to convert is 3,999,999,999, which does 33 iterations of the subtraction loop overall. In essence, one trades each division-by-ten-with-remainder operation, for up to nine subtractions. (This does not count keeping the iteration count – which is always less than 10, or between 48 and 57 if using ascii digits '0'..'9', nor updating the length of the buffer etc., since those should be "native" but the subtraction may be between much wider integers than fit in a single machine register.)
Even implementing the conversion via repeated subtraction for 64-bit integers isn't horribly slow, since even the slowest values like 17,999,999,999,999,999,999 only need 170 iterations of the subtraction loop.
An interested Arduino programmer might wish to try the above on their favourite 8-bit architecture, and see what the timing/efficiency is like, and compare to the conversion functions provided by the base libraries (snprintf() or the String() constructor in Arduino, for example).
Whenever I discover myself needing some kind of integer to string conversion in a very constrained situation (i.e., without existing conversion functions I can use), I do often end up implementing some crude conversion first, then cleaning it up later – not because of laziness, but because I need to see what is needed and useful first, before I am willing to commit to a specific approach. Just like the Linux kernel developers, who steadfastly refuse any idea of an internal ABI just because it would bind their hands to such ABIs, I too want to keep my options as open as possible, whenever I'm dealing with very tightly constrained situations like interrupt handlers and POSIX signal handlers.
One important takeaway for those learning C from this and my previous post above, is that there is no "best approach". I've shown some of the tradeoffs I make in certain situations, but the process of first finding out what kind of tradeoffs are possible, and then making informed choices, is the interesting bit.
My initial choices are often wrong, because I learn as I create. There is nothing bad about that, and indeed I do not even notice, because I try to keep such choices refactorable (https://en.wikipedia.org/wiki/Code_refactoring), and if I have time, sometimes refactor code just to see if I have learned enough in between (writing the original code and when I decide to refactor) to make a difference. It reminds me of trying new recipes and techniques when cooking, really.
And to continue the data spew, a quick look at those normal, non-constrained situations a C programmer should know and reach for first.
Not all of these are defined in the C standard. Some are defined in POSIX, and some are very commonly available GNU and/or BSD extensions. On the rare architectures they do not exist, it is relatively straightforward (but dull, careful work and testing) to implement these in terms of standard C functions. My links point to the Linux man pages online (https://www.man7.org/linux/man-pages/) project, but only because it very carefully and systematically describes which standards a function conforms to (under the Conforming to heading), plus notes, bugs, oddities, and implementation differences. I am not pushing you towards Linux, I've just found it more reliable and up to date than any of the alternatives like linux.die.net/man or unix.com/man-page, although those do have some not listed in Linux man pages, especially the latter wrt. functions not implemented in any Linux variants, for example those only on OpenSolaris for example).
These do have the same limitations as normal C printf() family of functions have, and none of these are async-signal safe (https://man7.org/linux/man-pages/man7/signal-safety.7.html).
- printf() (https://man7.org/linux/man-pages/man3/printf.3.html), fprintf() (https://man7.org/linux/man-pages/man3/fprintf.3.html), vprintf() (https://man7.org/linux/man-pages/man3/vprintf.3.html), and vfprintf() (https://man7.org/linux/man-pages/man3/vfprintf.3.html) (Standard C)
These are the most commonly used functions of the printf family. Plain printf() prints to standard output, and fprintf() to any stream (FILE *).
The parameters the formatting specification refers to are passed as extra parameters to fprintf() and printf(), and as a variable argument list to vfprintf() and vprintf() (see <stdarg.h> (https://man7.org/linux/man-pages/man3/stdarg.3.html)).
- snprintf() (https://man7.org/linux/man-pages/man3/snprintf.3.html) and vsnprintf() (https://man7.org/linux/man-pages/man3/vsnprintf.3.html) (Standard C)
These take a pointer to a buffer, the size of that buffer, a formatting string, and either the parameters needed by the formatting string, or a variable argument list containing those (see <stdarg.h> (https://man7.org/linux/man-pages/man3/stdarg.3.html)). They return the number of characters needed to describe the string (not including a terminating nul), or negative in case of an error.
If the buffer is not large enough, the return value will be the buffer size or larger, but the function will not overwrite memory past the specified size. This means that if the return value matches the buffer size, the buffer is NOT terminated with a nul char, and was not large enough. It is easy to get this logic wrong if you are not aware of it, but here is a snippet as an example of proper handling:
char buf[10];
int len = snprintf(buf, sizeof buf, "%d", X);
if (len < 0) {
/* snprintf() reported an error. Nothing useful in buf[]. */
} else
if ((size_t)len < sizeof buf) {
/* We have the decimal representation of X in buf as a string.
There is a terminating nul char at buf[len]. */
} else {
/* buf was too small to hold the entire string.
len may be much bigger than sizeof buf,
so do not assume buf[len] can be accessed.
There is no particular reason to assume that
buf[sizeof buf - 1] == '\0'
and it almost never is. */
}
- dprintf() (https://man7.org/linux/man-pages/man3/dprintf.3.html) and vdprintf() (https://man7.org/linux/man-pages/man3/vdprintf.3.html) (POSIX, was GNU long time ago)
These are the printf family functions you can use to format and write a string to a raw descriptor (without the standard I/O stream abstraction represented by FILE * handles). Typically, you do clear errno to zero before calling these, to be able to differentiate between printf formatting errors and I/O errors; and I only recommend using these on files, character devices, and stream sockets, not on datagram sockets. For datagram sockets, using asprintf()/vasprintf() and then send() on the properly constructed message, is the proper way to ensure the entire message is sent correctly (since send() on datagram sockets do not return short counts in some non-error situations like write() does), and lets you the programmer differentiate between message formatting ("printf-related") and connection I/O issues.
- strftime() (https://man7.org/linux/man-pages/man3/strftime.3.html) (POSIX), the Swiss Army Knife for formatting timestamps in a struct tm structure.
You should use clock_gettime(CLOCK_REALTIME, ×pec) (https://man7.org/linux/man-pages/man3/clock_gettime.3.html) to obtain the Unix Epoch time (same as time() and gettimeofday() report) at nanosecond resolution, then convert the .tv_sec to a struct tm using localtime_r() (https://man7.org/linux/man-pages/man3/localtime_r.3.html) (if you want the readable form in local time) or gmtime_r() (https://man7.org/linux/man-pages/man3/gmtime_r.3.html) (if you want the readable form in UTC/GMT or the closest equivalent). The only downside with strftime() is that since struct tm does not have a field for fractions of seconds (like struct timespec and struct timeval have), you are limited to second granularity in your timestamps.
If you have multiple 'clients' your code is connected to or services, use newlocale() (https://man7.org/linux/man-pages/man3/newlocale.3.html) to get a separate locale_t for each one, then use uselocale() (https://man7.org/linux/man-pages/man3/uselocale.3.html) before using localtime_r(). uselocale() is thread-specific, like errno, so only affects the current thread.
If you intend your code to be localizable, i.e. messages and date and timestamps confugurable to each locale and language via gettext() (https://man7.org/linux/man-pages/man3/gettext.3.html), this function is indispensable, because you only need to make the strftime() format pattern a gettext message, and those creating translations and localizations can then define the date/time format in the localization catalog for this program. Very powerful, even if limited to one-second precision!
- asprintf() (https://man7.org/linux/man-pages/man3/asprintf.3.html) and vasprintf() (https://man7.org/linux/man-pages/man3/vasprintf.3.html) (GNU, BSD)
These functions dynamically allocate a buffer large enough to hold the result, that pointer stored to the location pointed by the first parameter. The second parameter is the familiar printf() family formatting string. asprintf() takes additional parameters just like printf() does, and vasprintf() takes a variable argument list (see <stdarg.h> (https://man7.org/linux/man-pages/man3/stdarg.3.html)). They return the number of chars in the buffer, not including the terminating nul byte, or negative if an error occurs. BSD and GNU behave a bit differently if that happens: BSD resets the buffer pointer to NULL, while GNU leaves it unmodified.
I personally warmly recommend the following pattern:
char *buffer = NULL;
errno = 0;
int length = asprintf(&buffer, ...);
if (length >= 0) {
/* No problems; buffer[length] == '\0' */
do_something_with(buffer, length);
free(buffer);
} else {
/* Error; see 'errno' for cause.
You don't need to, but it is safe to
buffer = NULL;
here; you won't leak memory. */
}
If the <stdarg.h> "variable argument list" stuff sounds odd to you, consider the following working example snippet:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <errno.h>
static volatile int my_logging_oom = 0;
/* Log an error with printf formatting support.
Returns 0 if success, errno error code otherwise. */
int my_logging_function(const char *fmt, ...)
{
va_list args;
char *message = NULL;
int len;
va_start(args, fmt);
len = vasprintf(&message, fmt, args);
va_end(args);
if (len < 0) {
my_logging_oom = 1;
return errno = ENOMEM;
}
somehow_log_message(message, len);
free(message);
return 0;
}
The #defines tell the C library on Linux to expose both POSIX and GNU extensions in the header files included. BSDs expose them by default.
The my_logging_oom variable is just a volatile flag used to record if logging ever fails due to Out Of Memory. I'd expect other code to examine it every now and then, and report to the user if it ever becomes nonzero.
The only "trick" with this kind of variadic functions is that their parameters go through default argument promotion, as described in the C standard. Essentially, both float and double are passed as double . Any integer types smaller than int will be converted to int if that retains the value, and to unsigned int otherwise. Fortunately, this does not affect pointers: a pointer to a float is passed as a pointer to a float, because pointers are not subject to default argument promotion. It doesn't affect arrays either, because they decay to a pointer to their first element, so passing a name of an array to variadic function is the same as passing a pointer to the first element of that array.
So, if you wanted, you definitely could implement your own printf() family of functions using <stdarg.h>. However, as SiliconWizard mentioned, this formatting approach is not always superior to just constructing the message piece by piece, using type-specific functions call for each conversion. Aside from ease of use, the one truly useful thing is making the printf format specification be a gettext() (https://man7.org/linux/man-pages/man3/gettext.3.html) message, so that end users can very easily translate and localize the program without recompiling the binaries and adding code. A practical example:
#include <stdlib.h>
#include <locale.h>
#include <string.h>
#include <stdio.h>
#include <libintl.h>
#define _(msgid) (gettext(msgid))
int main(int argc, char *argv[])
{
/* This program is locale-aware. */
setlocale(LC_ALL, "");
/* Let's call ourselves 'greeting', so that if you want, you can put
a message catalog at say /usr/share/locale/<yourlocale>/LC_MESSAGES/greeting.mo
*/
textdomain("greeting");
if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr, _("\n"
"Usage: %1$s [ -h | --help ]\n"
" %1$s NAME1 NAME2\n"
"\n"
"This prints a localizable greeting.\n"
"\n"), arg0);
return EXIT_FAILURE;
}
const char *name1 = (argv[1][0]) ? argv[1] : _("(emptyname1)");
const char *name2 = (argv[2][0]) ? argv[2] : _("(emptyname2)");
printf(_("Hey %1$s, %2$s sends their greetings.\n"), name1, name2);
return EXIT_SUCCESS;
}
Note that a formatting directive that begins with say %3$ means "the first variadic argument", whereas % means "the next variadic argument". You can use either one in any printf formatting string, but you cannot and must not mix the two forms. As an example, %3$d formats the third variadic argument as an int, and %2$s the second variadic argument as a string.
A message catalog for say "Formal English" might replace the "Hey .... message with say "Greetings from %2$s to %1$s.\n" .
I personally do not like using a _(msgid) macro at all, and much prefer say MESSAGE() or LOCALIZE() instead. The reason I kept it in above, is that it is common pattern in C that confuses those who don't know about it beforehand, so I thought it as a good idea to stuff that in there as well.
If you want to play with the above, save the following as greeting.po:
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid ""
"\n"
"Usage: %1$s [ -h | --help ]\n"
" %1$s NAME1 NAME2\n"
"\n"
"This prints a localizable greeting.\n"
"\n"
msgstr ""
"\n"
"Usage: %1$s [ -h | --help ]\n"
" %1$s NAME1 NAME2\n"
"\n"
"This prints a localizable greeting from NAME2 to NAME1.\n"
"\n"
msgid "(emptyname1)"
msgstr "(unknown person)"
msgid "(emptyname2)"
msgstr "(unknownn person)"
msgid "Hey %1$s, %2$s sends their greetings.\n"
msgstr "Greetings from %2$s to %1$s.\n"
where msgid describes the exact key the program is looking for, and msgstr the replacement for this message catalog.
(If there is no message catalog, msgid is used as-is. That's why it looks a bit funky at a first glance. It's a very simple, easy to manage format, though.)
You can 'compile' the human-readable greeting.po into an efficient binary message catalog file greeting.mo using
msgfmt greeting.po -o greeting.mo
and install that to say the en_GB locale via
sudo install -m 0644 -o root -g root greeting.mo /usr/share/locale/en_GB/LC_MESSAGES/
You can then compare the program output when run with different locales:
LC_MESSAGES=C ./greeting
LC_MESSAGES=en_GB.utf8 ./greeting
Do check locale -a output and the /usr/share/locale/ directory to see which locales you use. Many Linux distributions use the .utf8 suffix to denote UTF-8 locales, but there are alternate ways, so the above might not apply exactly as-is to yours.
Obviously, there are much better tools and even IDEs for maintaining and dealing with message catalogs; the above is just the most basic functioning example I could put together. Interesting stuff, anyway, and perhaps important as a counterpoint to why/when one should use the standard tools for string formatting, instead of rolling ones own.
I really struggle to think of or even imagine any machine in which repeating a divide by ten is going to be cheaper than copying a character from one place to another. If an actual divide instruction is used that's usually going to be at least 32 clock cycles on a 32 bit machine.
Even if you use n/10 = (n*0xCCCCCCCD)>>35 that's not super cheap because you need a 32x32->64 multiplier (or MUL and MULH instructions).
It's going to be cheaper to do something like:
digits = 1;
tst = 10;
while (digits < 10 && n >= tst){
digits++;
tst *= 10;
}
The multiply by 10 will turn into something like either tst = (tst<<1) + (tst<<3) or tst += tst<<2;tst <<= 1 depending on the instruction set.
But, seriously, copying characters from a format-number-backwards buffer to the message-construction-buffer is super quick so there's no reason not to do it.
I really struggle to think of or even imagine any machine in which repeating a divide by ten is going to be cheaper than copying a character from one place to another. If an actual divide instruction is used that's usually going to be at least 32 clock cycles on a 32 bit machine.
Very good point! Of course, if you don't mind a power of ten look up table (for 10=10¹, 100=10²) – 36 bytes for 32-bit –, it turns to just a set of comparisons.
I think uint32_digits() is one of those rare functions better written in (extended inline) assembly on 8-bit MCUs. On 32-bit it is trivial (binary decision tree with nine comparisons and ten results); and on 16-bit is two decision trees with a bit of overlap; so those need no hand-holding, and even simple C code should compile to pretty efficient machine code.
I got sucked down a rabbit hole trying to find the minimum-depth decision tree (ternary tree, with < = > leaves); even wrote some Python3 code for formal analysis (with a tunable cost model) for arithmetic comparison rule decision trees... :palm:
I get so excited when I get to chew on a type of problem I haven't chewed on in a while, you see.
On 8-bit MCUs, 32-bit integers span four bytes; the values we need to compare against are
1000000000 = 3b 9a ca 00 = 10⁹; at and above, the result is 10
100000000 = 05 f5 e1 00 = 10⁸; at and above, the result is 9
10000000 = 00 98 96 80 = 10⁷; at and above, the result is 8
1000000 = 00 0f 42 40 = 10⁶; at and above, the result is 7
100000 = 00 01 86 a0 = 10⁵; at and above, the result is 6
10000 = 00 00 27 10 = 10⁴; at and above, the result is 5
1000 = 00 00 03 e8 = 10³; at and above, the result is 4
100 = 00 00 00 64 = 10²; at and above, the result is 3
10 = 00 00 00 0a = 10¹; at and above, the result is 2
with the result being 1 otherwise.
If the most significant byte is 3c..ff, the result is always 10.
If the most significant byte is 3b, the result is either 9 or 10, depending on the three other bytes.
If the most significant byte is 06..3a, the result is 9.
If the most significant byte is 05, the result is either 8 or 9 depending on the three other bytes.
If the most significant byte is 01..04, the result is 8.
Otherwise, the most significant byte must be zero, and the result depends on the three other bytes.
For the most significant byte, the decision tree involves comparing against 3b, 05, and 00.
So, one of the most efficient approaches is to first compare the most significant byte to 05, and cascade from there on down in a ternary tree (with 'less', 'equal', and 'greater' edges from each node, each node comparing one of the four bytes to a constant).
The actual number of byte comparisons needed to find any single answer is surprisingly small. Code that does the comparisons in linear order will be almost as efficient, depending somewhat on the instruction set. For the most efficient approaches, the code ends up being write-only: nobody will ever debug or modify it, only rewrite if needed.
I don't mind, because I personally test all 32-bit conversion functions with all possible inputs, and for larger input spaces, I apply some sequential unit testing over any regions where the algorithm changes (like arguments between 9999..99999 above, because they span both 16- and 24-bit arguments), plus randomized testing over the entire input range. So I know what I trust or not, but cow-orkers might balk. But that's why I like writing low-level utility functions: once implemented this way, they are efficient and trustworthy.
But, seriously, copying characters from a format-number-backwards buffer to the message-construction-buffer is super quick so there's no reason not to do it.
Yes, and since I suspected there is much more experience behind that statement than one might think, I went looking. And you're absolutely right.
One use case I used that approach with was when I needed to emit the digits in left-to-right order to some device, say an UART, or parallel GPIO (stealing the high bit as a clock/write strobe, so two writes per byte), but don't want to keep a buffer in the first place (because it was a quick-and-dirty data stream, emitted from a interrupt context). On AVR, conversion uses so many registers (according to code emitted by gcc and llvm/clang) that just using a buffer, even one on stack, uses less RAM. So, the approach is counterproductive there, buffer is more efficient even memory-use-wise.
On 32-bit architectures like ARMs, spilling three registers into the stack takes up the memory you need to buffer any 32-bit signed or unsigned integers. If the code that generates the string without a RAM buffer uses three registers that otherwise would not have needed to be saved and restored, one just does extra work without any gain, having to pay the same RAM overhead due to clobbering more registers than the buffer would have taken. Since pointers etc. fit in single registers, the conversion-to-buffer code is particularly simple.
On AMD64 and 64-bit arches, the buffer is basically free. You can do register-based conversion there, holding up to 8 ASCII characters in a single register, because of the sheer size of the registers, but using even two extra 64-bit registers that otherwise would not have been clobbered (and therefore not saved and restored), takes up more memory than a typical 32-bit integer conversion string buffer. For 64-bit integers, the buffer needs less RAM than storing and restoring three registers.
So, it isn't even that "this is simpler and faster", but more like "If you check, you'll almost always find you didn't save any memory by doing it the slow, complicated way".
In very memory constrained situations using BCD would halve the buffer size needed, and given a suitably aligned easily accessible 16-entry lookup table, one can stuff non-digit stuff like implicit or explicit padding, decimal points, thousands separators, and plus or minus signs in them very easily.
(Those who are unfamiliar: In Binary Coded Decimal, byte value v consists of two decimal digits a=0..9 and b=0..9 via v=a≪4+b, where ≪ is binary shift left. The ASCII character corresponding to b is (v&15)+48, and the one corresponding to a is (v≫4)+48. a is a valid decimal digit if v is less than 160. If you use (((v≫4)+6)&15)+42 and ((v+6)&15)+42, then 10-15 encode ASCII characters * + , - . /. Yet another reference to the good ol' forty-two.. A lot of old tricks used with BCD have been forgotten.)
Anyone know how to put code pieces inline on this board?
I use [tt]..[/tt] for the teletype look. The forum software retains newlines inserted in the editor so for example
[tt] foo = 2[sup]5[/sup][/tt]
[tt] bar = 11100100[sub]2[/sub][/tt]
renders nicely as
foo = 25
bar = 111001002
I always wonder why it's not popular to implement "custom" versions of sprintf() that behave the way a particular application needs, but doesn't include the features not needed... I mean, there's a well-established framework for implementing it (though varargs can be expensive), and it's likely to be immediately recognized by anyone reading the code.
sprintfFoo(output, "%d:%02d:%02d:%03d", hours, minute, seconds, millis);
Division too expensive? Only implement hex... etc.
I always wonder why it's not popular to implement "custom" versions of sprintf() that behave the way a particular application needs, but doesn't include the features not needed... I mean, there's a well-established framework for implementing it (though varargs can be expensive), and it's likely to be immediately recognized by anyone reading the code.
sprintfFoo(output, "%d:%02d:%02d:%03d", hours, minute, seconds, millis);
Division too expensive? Only implement hex... etc.
If you only need to format characters, strings, and register-sized integers then printf() can be pretty small. Supporting field widths and padding options adds a little but but not bad. It's floating point capability that bloats things whether you're using it of not, especially if you don't have floating point hardware. And integers bigger than your registers.
Newlib Nano has a cut-down printf. Most embedded toolchains use NewLib and you enable Nano by adding "—specs=nano.specs" to CFLAGS and LDFLAGS.
C++20 adds a std:format() stream function :-)
cout << millis() << ": " << format("The answer is {}.", 42) << endl;
I also don't like this overloading of << for streams. Would it be that much worse to write instead of the above?
cout.put(millis()).put(": ").format("The answer is {}.", 42).endl();
Either way comes down to the exact same thing at the compiled code level.
The advantages of the C++ way are extensibility, type safety, and not dragging in the formatters and libraries for floating point (for example) if you don't use them.
But it's SO MUCH more cumbersome to write than printf()-style formatting. And defeats localisation that wants to change not only the words or formats for dates etc but also wants to change the order of the elements.
A *proper* objected-oriented language can separate the conversion of values into strings from the interpolation of those strings into the format string e.g. by using a toString() method, possibly with arguments for field width, decimal places, padding character etc.
But in C++ characters and strings and numbers are not instances of a class :-(