Skip to content

Format & Print

Overview

ETL provides text formatting facilities modelled on C++20 std::format and C++23 std::print. They allow type-safe, positional formatting of values into strings or directly to a character output device — without heap allocation.

Minimum language standard: C++11 (ETL_USING_CPP11).

Headers:

HeaderProvides
etl/format.hetl::format_to, etl::format_to_n, etl::formatted_size
etl/print.hetl::print, etl::println (includes etl/format.h)

etl::format_to

Generic output-iterator overload

template <typename OutputIt, class... Args>
OutputIt format_to(OutputIt out, format_string<Args...> fmt, Args&&... args);

Formats args according to the format string fmt and writes the result through the output iterator out. Returns an iterator past the last character written.

OutputIt can be any output iterator whose dereferenced type is assignable from char, for example etl::istring::iterator or etl::back_insert_iterator<etl::istring>.

etl::string<100> s;

// Using a raw iterator — you must resize the string yourself
etl::istring::iterator result = etl::format_to(s.begin(), "{0} {1}", 34, 56);
s.uninitialized_resize(static_cast<size_t>(result - s.begin()));
// s == "34 56"

// Using a back_insert_iterator — string grows automatically
s.clear();
etl::back_insert_iterator<etl::istring> it(s);
etl::format_to(it, "{} {}", 65, 34);
// s == "65 34"

etl::istring& overload (ETL-specific)

template<class... Args>
etl::istring::iterator format_to(etl::istring& out,
                                  format_string<Args...> fmt,
                                  Args&&... args);

Convenience overload that writes into an etl::istring (or any derived etl::string<N>). The string is automatically resized to the number of characters written, up to out.max_size(). Returns an iterator past the last character written.

etl::string<100> s;
etl::format_to(s, "Hello, {}!", "world");
// s == "Hello, world!"

etl::format_to_n

template<typename OutputIt, class... Args>
OutputIt format_to_n(OutputIt out, size_t n,
                     format_string<Args...> fmt, Args&&... args);

Like format_to, but writes at most n characters. Characters beyond the limit are silently discarded.

etl::string<10> s = "abcdefghij";
etl::format_to_n(s.begin(), 3, "xy{}", 123);
// s == "xy1defghij"   (only 3 chars written)

etl::formatted_size

template<class... Args>
size_t formatted_size(format_string<Args...> fmt, Args&&... args);

Returns the total number of characters that format_to would produce, without actually writing anything. Useful for pre-computing buffer sizes.

size_t n;
n = etl::formatted_size("");            // 0
n = etl::formatted_size("{}", "");      // 0
n = etl::formatted_size("xyz{}", 12);  // 5
n = etl::formatted_size("{}", "abc");   // 3

etl::print and etl::println

Declared in etl/print.h.

etl::print

template<class... Args>
void print(etl::format_string<Args...> fmt, Args&&... args);

Formats the arguments and outputs each character by calling etl_putchar().

etl::println

// With arguments — prints formatted text followed by '\n'
template<class... Args>
void println(etl::format_string<Args...> fmt, Args&&... args);

// Without arguments — prints a bare newline
void println();

Implementing etl_putchar

etl/print.h declares (but does not define) the following C-linkage function:

extern "C" void etl_putchar(int c);

You must provide a definition in your project. The int parameter follows the convention of the standard putchar() and carries a single char value.

Typical implementations forward to a UART, a debug probe, putchar, or any other single-character output sink:

// Example: forward to standard putchar
extern "C" void etl_putchar(int c)
{
  putchar(c);
}

Example

etl::print("x = {}, y = {}\n", 10, 20);   // "x = 10, y = 20\n"
etl::println("Hello, {}!", "world");        // "Hello, world!\n"
etl::println();                             // "\n"

Format String Syntax

A format string is ordinary text with replacement fields delimited by braces:

"literal text {} more text {1:>10} end"

Replacement field grammar

replacement_field ::= '{' [arg_id] [':' format_spec] '}'
arg_id            ::= integer            // e.g. 0, 1, 2 …
format_spec       ::= [[fill]align] [sign] ['#'] ['0'] [width] ['.' precision] ['L'] [type]
ComponentSyntaxDescription
Argument index{0}, {1}, …Manual positional indexing. Cannot be mixed with automatic indexing.
Automatic index{}Uses the next argument in order. Cannot be mixed with manual indexing.
Fill characterany character except { or }Used together with an alignment specifier. Default is space ( ).
Alignment< left, > right, ^ centerAligns the formatted value within the given width.
Sign+ always, - negative only (default), space for positiveControls sign display for numeric types.
# (alt form)#Adds 0x/0X for hex, 0b/0B for binary, 0 for octal.
0 (zero-pad)0Pads the number with leading zeros (after sign/prefix).
Widthinteger, or {} / {n}Minimum field width. Supports nested replacement fields for dynamic width.
Precision. integer, or .{} / .{n}For strings: maximum characters to output. For floats: number of decimal digits. Supports nested replacement fields.
LLLocale-specific flag (parsed but currently ignored).
Typesee Presentation TypesSelects the output representation.

Examples

etl::format_to(s, "{:>10}", 42);       // "        42"
etl::format_to(s, "{:*^10}", 42);      // "****42****"
etl::format_to(s, "{:+05d}", 67);      // "+00067"
etl::format_to(s, "{:#x}", 0x3f4);     // "0x3f4"
etl::format_to(s, "{:.3s}", "abcdef"); // "abc"
etl::format_to(s, "{1} {0}", 1, 2);    // "2 1"

Supported Argument Types

The core set of formattable types (matching std::basic_format_arg):

CategoryTypes
Booleanbool
Characterchar
Signed integerint, long long int
Unsigned integerunsigned int, unsigned long long int
Floating-point (opt-in)float, double, long double — requires ETL_USING_FORMAT_FLOATING_POINT
Stringconst char*, etl::string_view
Pointerconst void*

Implicit conversions

Types not listed above are converted automatically before formatting:

Source typeStored as
shortint
unsigned short, uint16_tunsigned int
long intint or long long int (platform-dependent)
unsigned long int, size_tunsigned int or unsigned long long int
int8_t (signed char)char
uint8_t (unsigned char)char
int16_tint
uint32_tunsigned int
int32_tint
etl::string<N>etl::string_view (lifetime of the temporary is guaranteed)
any pointer T*const void*

Presentation Types per Argument Kind

Integers (int, unsigned int, long long int, unsigned long long int)

TypeMeaningExample
d (default)Decimal134"134"
xLowercase hexadecimal0x3f4"3f4"
XUppercase hexadecimal0x3f4"3F4"
oOctal034"34"
bLowercase binary0b1010"1010"
BUppercase binary0b1010"1010"
cCharacter (value as char)67"C"

With #: prefixes 0x/0X, 0b/0B, or leading 0 for octal.

Characters (char, signed char, unsigned char)

TypeMeaningExample
c (default)Character itself's'"s"
?Debug / escaped'\n'"'\\n'"
dDecimal code point'a'"97"
x / XHex code point'a'"61"

Booleans (bool)

TypeMeaningExample
(default)false / truetrue"true"
sSame as defaulttrue"true"
d0 / 1true"1"
x / XHex 0 / 1true"1"
oOctal (with #: 01)true"01"

Strings (const char*, etl::string_view, etl::string<N>)

TypeMeaningExample
s (default)String output"data1""data1"
?Debug / escaped"data1\n""\"data1\\n\""

Width and precision apply: width sets the minimum field width; precision (.N) truncates the string to at most N characters.

etl::format_to(s, "{:>10s}", "data1");    // "     data1"
etl::format_to(s, "{:.3s}", "abcdef");    // "abc"
etl::format_to(s, ".{:^8.3s}!", "data1"); // ".  dat   !"

Pointers (const void*)

TypeMeaningExample
p (default)Lowercase hex with 0x prefixnullptr"0x0"
PUppercase hex with 0X prefixnullptr"0X0"

Floating-point (float, double, long double)

Requires ETL_USING_FORMAT_FLOATING_POINT.

TypeMeaningExample
(default)Shortest representation1.5f"1.5"
e / EScientific notation1.0f"1.000000e+00"
f / FFixed-point notation1.125f"1.125000"
g / GGeneral (fixed or scientific)1e10f"1.000000e+10"
a / AHexadecimal floating-point1.5f"0x1.8p+0"

nan, inf (lowercase for e/f/g/a, uppercase for E/F/G/A).

Formatting User-Defined Types

Custom types can be made formattable by providing a specialisation of etl::formatter<T>, mirroring the std::formatter<T> mechanism. Once a specialisation is in scope, values of that type can be passed to format_to, format_to_n, formatted_size, print, and println exactly like the built-in types.

The etl::formatter primary template

template <class T, class CharT = char>
struct formatter;

The primary template is empty. A type T becomes formattable when you provide a specialisation (in namespace etl) that defines two member functions:

MemberSignaturePurpose
parseformat_parse_context::iterator parse(format_parse_context& parse_ctx)Parses the format-spec portion of the replacement field (the characters after :, up to the closing }). Returns an iterator to the first unconsumed character, which must be the closing }. Return parse_ctx.begin() to consume nothing.
formattemplate <class OutputIt> format_context<OutputIt>::iterator format(const T& value, format_context<OutputIt>& fmt_ctx)Writes the formatted representation of value through fmt_ctx.out() and returns the iterator past the last character written.

Context types

TypeDescription
etl::format_parse_contextAlias for etl::basic_format_parse_context<char>. Provides begin(), end(), and advance_to() over the format-spec characters. Its iterator is etl::string_view::const_iterator.
etl::format_context<OutputIt>Alias for etl::basic_format_context<OutputIt, char>. Provides out() (the current output iterator) and advance_to(). Its iterator is OutputIt.

Example

// A simple user-defined type.
struct coordinate
{
  int x;
  int y;
};

// Specialise etl::formatter for it.  Here format() simply delegates to
// etl::format_to, writing the two members as "(x, y)".
namespace etl
{
  template <>
  struct formatter<coordinate>
  {
    format_parse_context::iterator parse(format_parse_context& parse_ctx)
    {
      return parse_ctx.begin();
    }

    template <class OutputIt>
    typename format_context<OutputIt>::iterator format(const coordinate& c, format_context<OutputIt>& fmt_ctx)
    {
      return etl::format_to(fmt_ctx.out(), "({}, {})", c.x, c.y);
    }
  };
}

With the specialisation in scope, coordinate behaves like any other argument:

etl::string<100> s;
coordinate c{3, 7};

etl::format_to(s, "{}", c);                // "(3, 7)"
etl::format_to(s, "point={}", c);         // "point=(3, 7)"
etl::format_to(s, "{0} and {0}", c);      // "(3, 7) and (3, 7)"
etl::format_to(s, "{} {} {}", 'a', c, 5); // "a (3, 7) 5"
etl::formatted_size("{}", c);             // 6

How it works

When an argument type provides an etl::formatter<T> specialisation, ETL stores the value type-erased inside basic_format_arg::handle rather than in one of the built-in alternatives. During formatting the handle invokes the formatter’s parse and format members. Detection is automatic: a type is treated as a custom type when etl::formatter<T> exposes a callable parse() member, so no opt-in trait or registration is required.

Escape Sequences and Literal Braces

Literal braces

Because { and } delimit replacement fields, they must be escaped by doubling:

InputOutput
{{{
}}}
etl::format_to(s, "abc{{def");  // "abc{def"
etl::format_to(s, "}}abc");     // "}abc"

Debug / escaped presentation (?)

The ? type specifier produces a debug representation:

  • Characters are wrapped in single quotes with C-style escape sequences:

    CharacterOutput
    \t'\\t'
    \n'\\n'
    \r'\\r'
    "'\\\"'
    ''\\''
    \\'\\\\'
  • Strings are wrapped in double quotes with the same escape sequences:

    etl::format_to(s, "{:?}", "data1\n");  // "\"data1\\n\""
    

Error Handling

Invalid format strings cause an etl::bad_format_string_exception (derived from etl::format_exception, which is derived from etl::exception).

Common error conditions:

ConditionExample
Missing closing brace"a{b"
Unescaped } without matching {"a}b"
Invalid characters inside {}"a{b}"
Argument index out of range"{1}" with only one argument
Mixing manual and automatic indexing"{0} {}"
Invalid type specifier for the argument"{:d}" on a string_view
Double colon in format spec"{::}"
Precision on an integer"{:+#05.5X}" on an int
etl::string<100> s;
// These all throw etl::bad_format_string_exception:
etl::format_to(s, "a{b}",  1);      // bad index spec
etl::format_to(s, "a{b",   1);      // closing brace missing
etl::format_to(s, "a}b");           // unescaped }
etl::format_to(s, "{:d}", sv);      // invalid type for string_view

Note: On C++20 and later, compile-time format string validation through consteval is planned but not yet fully implemented.

Differences from std::format

Areastd::format (C++20/23)ETL
Output targetReturns std::stringWrites through an output iterator or into etl::istring& — no heap allocation.
etl::istring& overloadNot availableformat_to(etl::istring&, ...) automatically resizes the string.
print / println outputWrites to FILE* / stdoutWrites character-by-character via user-defined etl_putchar(int).
Floating-point supportAlways availableOpt-in via ETL_USING_FORMAT_FLOATING_POINT.
User-defined formattersstd::formatter<T> specialisationsSupported via etl::formatter<T> specialisations (see Formatting User-Defined Types).
LocaleL flag uses std::localeL flag is parsed but has no effect.
Compile-time validationEnforced via consteval on C++20Planned; currently validates at run time and throws etl::bad_format_string_exception.
format_to_n return typestd::format_to_n_resultReturns the underlying OutputIt directly.