February 3, 2025

Introducing bsl::format

BDE 4.20 introduces bsl::format an implementation of the standard library format facility (introduced in C++20)

In general, the bsl_format.h header provides bsl equivalents to the the free functions found in the Standard Library <format> header.

introductory example
bsl::cout << bsl::format("{} {}!", "Hello", "World");

C++20’s std::format facility is based on the Open Source {fmt} library, which is itself based on Python’s str.format facility. This facility combines the readability and ease of use of printf with the type safety and extensibility of iostreams.

The BSL implementation for format additionally:

  • Provides formatters for key Bloomberg vocabulary types

  • Returns a bsl::string

  • Supports C++03

  • Supports allocator-aware overloads

Useful Links

We consider bsl::format appropriate for production use, but additional work is required to bring the implementation up to our “gold” standard. Remaining work includes:

  • More thorough documentation and usage examples.

  • Complete BDE test drivers for all components (some components may currently have only breathing tests)

Comparing format with printf

Similar to printf, bsl::format calls are easy to read, and provide a separation between the format string and its arguments. It shares the same general philosophy of using placeholders in a format string which are replaced by values.

Unlike printf, bsl::format is memory safe, type safe, and can be extended to support user-defined types. It also provides full format string error checking, at compile time (for static format strings on a modern compiler) or at runtime (otherwise). This makes bsl::format a safer, more extenstible, and potentially more performant alternative to printf.

Comparing format with iostreams

C++ iostreams provide a facility that is type safe and very easy to extend for custom types, but also has a number of drawbacks.

  • iostreams can be comparatively inefficient.

  • iostreams can be difficult to read and use because variable values are interleaved with the text in ad-hoc ways making controlling the rendering and handling localization more difficult.

Consider the following example:

A more complicated format specification to localize with iostreams
bsl::cout << "value a (" << a << ") is less than b (" << b << ")";
A more complicated format specification to localize with printf and format
printf("value a (%d) is less than b (%d)", a, b);
format("value a ({}) is less than b ({})", a, b);

Notice, that for printf and format, it would be easier to create a table of localized format strings because there is a single format string per message.

The rendering of iostreams is also complicated by an inconsistent approach between standard library manipulators.

Consider the following example:

bsl::cout << "0x"
          << bsl::hex
          << bsl::setfill('0')
          << bsl::setw(4)
          << value << '\n';  // "0x002a"

bsl::cout << value << '\n';  // What does this print??

Notice that rendering the number as hexidecimal is relatively verbose. Also notice that the second cout will output “2a” because hex is “sticky” and setfill and setw are not.

Unlike iostreams, bsl::format provides a more efficient facility whose rendering is easier to control, and whose text is generally easier to localize. The primary drawback for format (in comparison to iostreams) is that is harder to customize format for user-defined types.

Comparison between std::format and bsl::format

The Bloomberg Standard Library’s bsl::format provides an implementation of the C++20 standard format facility. As outlined in the introduction, bsl::format provides the following extensions:

  • bsl::format will provide support for common Bloomberg vocabary types (currently Decimal64, but with planned enhancements for bdlt date and time types).

  • bsl::format returns a bsl::string (not a std::string).

  • bsl::format is available on all platforms from C++03 onwards (including Sun).

  • bsl::format is fully allocator-aware, with additional overloads taking an allocator.

Note

On modern platforms bsl::format delegates most of its “heavy lifting” to std::format. On older platforms bsl::format is an entirely proprietary implementation.

As a result, on modern platforms the performance (and any behavioral quirks) of bsl::format mirrors that of the platform implementation.

Because of bsl::format support for legacy platforms, there are some behavioral notes:

  • User-defined formatters for bsl::format need to follow a few extra rules but, providing those rules are followed, they are fully compatible with std::format.

  • bsl::format string checking is done at runtime when built with C++ versions below C++20 and on older compilers.

  • locale-specific formatters are not (currently) provided for BSL types.

  • locale-specific overloads of bsl::format are only available on C++20 on modern compilers.

  • Runtime errors are reported by throwing a bsl::format_error rather than a std::format_error. Note, however, that on modern compilers bsl::format_error is an alias to std::format_error so catching bsl::format_error will work for both bsl::format and std::format.

Using bsl::format

Simple Example

A simple example of formatting text:

#include <bsl_format.h>

int main()
{
    int x = 42;
    bsl::cout << bsl::format("The value of {} is {}\n", "x", x);
    bsl::cout << bsl::format("Escaped left {{ and right }} braces\n");
    bsl::cout << bsl::format("Print a value in braces {{{}}}", x);
}
Program returned: 0
Program stdout
The value of x is 42
Escaped left { and right } braces
Print a value in braces {42}

Positional Placeholders

Positional placeholders are supported:

#include <bsl_format.h>

int main()
{
    double value     = 3.14159265359;
    int    width     = 8;
    int    precision = 3;
    bsl::cout << bsl::format("Pi is {2:{0}.{1}f}", width, precision, value) << bsl::endl;
    // Note: The numbers in the braces state which arguments to use

    bsl::cout << bsl::format("Pi is {:{}.{}f}", value, width, precision) << bsl::endl;
    // Note: The arguments are in the order of the (unescaped) opening braces '{'
}
Program returned: 0
Program stdout
Pi is    3.142
Pi is    3.142

Formatting Specifications

For standard types, the formatting specifications look remarkably like printf:

#include <bsl_format.h>

int main()
{
    double value = 3.14159265359;
    bsl::cout << bsl::format("Pi is {}", value) << bsl::endl;
    bsl::cout << bsl::format("Pi is {:f} default precision", value) << bsl::endl;
    bsl::cout << bsl::format("Pi is {0:f} default precision", value) << bsl::endl;
    bsl::cout << bsl::format("Pi is {:.4f} smaller precision", value) << bsl::endl;
    bsl::cout << bsl::format("Pi is {:.4e} exponential", value) << bsl::endl;
    bsl::cout << bsl::format("Pi is {:20.4e} in a wide field", value) << bsl::endl;
    // Note: the ':' character is required if a format spec is given
}
Program returned: 0
Program stdout
Pi is 3.14159265359
Pi is 3.141593 default precision
Pi is 3.141593 default precision
Pi is 3.1416 smaller precision
Pi is 3.1416e+00 exponential
Pi is           3.1416e+00 in a wide field

And they can be combined with positional placeholders:

#include <bsl_format.h>

int main()
{
    int day   = 23;
    int month = 1;
    int year  = 24;
    bsl::cout << bsl::format("The date in US format is {1:02}/{0:02}/{2:02}\n", day, month, year);
    bsl::cout << bsl::format("The date in UK format is {0:02}/{1:02}/{2:02}\n", day, month, year);
    // Note: ":02" indicates zero-padding and field width of 2
}
Program returned: 0
Program stdout
The date in US format is 01/23/24
The date in UK format is 23/01/24

Alignment and fill specifiers can also be used:

#include <bsl_format.h>

int main()
{
    double value = 3.14159265359;
    bsl::cout << bsl::format("Pi to the left is   '{:<20f}'", value) << bsl::endl;
    bsl::cout << bsl::format("Pi to the right is  '{:>20f}'", value) << bsl::endl;
    bsl::cout << bsl::format("Pi in the middle is '{:^20f}'", value) << bsl::endl;
    bsl::cout << bsl::format("Pi to the left is   '{:=<20f}'", value) << bsl::endl;
    bsl::cout << bsl::format("Pi to the right is  '{:=>20f}'", value) << bsl::endl;
    bsl::cout << bsl::format("Pi in the middle is '{:=^20f}'", value) << bsl::endl;
}
Program returned: 0
Program stdout
Pi to the left is   '3.141593            '
Pi to the right is  '            3.141593'
Pi in the middle is '      3.141593      '
Pi to the left is   '3.141593============'
Pi to the right is  '============3.141593'
Pi in the middle is '======3.141593======'

Runtime Defined Format Strings

Unfortunately using a format string that is defined at runtime is (slightly) more complicated (this is addressed in C++23). For non literal format strings you must use vformat:

#include <bsl_format.h>

int main()
{
    int         value = 42;
    bsl::string spec = "Value is {}\n";

    // The following won't compile:
    // bsl::cout << bsl::format(spec, value);

    // Need to use this instead:
    bsl::cout << bsl::vformat(spec, bsl::make_format_args(value));

    // Note the following would also give a compile-time error:
    // bsl::cout << bsl::vformat(spec, bsl::make_format_args(42));
    // (arguments have to be lvalues)
}

Runtime errors are indicated by an exception:

#include <bsl_format.h>

int main()
{
    int         value = 42;
    bsl::string spec = "Value is {:z}\n"; // Invalid format spec for int

    try {
        bsl::cout << bsl::vformat(spec, bsl::make_format_args(value));
    } catch (const bsl::format_error& err) {
        bsl::cout << bsl::format("The error is '{}'\n", err.what());
    }
}

Support for Allocators

Allocator aware overloads for bsl::format are available:

#include <bsl_format.h>

int main()
{
    bslma::TestAllocator ta("test");

    ASSERT(ta.numAllocations() == 0);

    bsl::string testStr = bsl::format(
                &ta,
                "Here we do a test with a long string and some number {}",
                42);

    ASSERT(testStr.get_allocator().mechanism() == &ta);
    ASSERT(ta.numAllocations() > 0);
}

Basic rules for formatting user defined types

This article does not go into the details of writing your own formatter. Writing a formatter for bsl::format is largely the same as writing one for std::format, with a few additional constraints the implementation must follow which are outlined below.

Below is a template for a formatter that is compatible with both bsl::format and std::format:

  #include <bsl_format.h>

  namespace bsl {
  template <class t_CHAR>
  struct formatter<MyType, t_CHAR> {
    public:
      template <class t_PARSE_CONTEXT>
      typename t_PARSE_CONTEXT::iterator  BSLS_KEYWORD_CONSTEXPR_CPP20 parse(
                                                        t_PARSE_CONTEXT&  pc)
      {
          // TODO
      }
      template <class t_FORMAT_CONTEXT>
      typename t_FORMAT_CONTEXT::iterator format(const MyType&      value,
                                                 t_FORMAT_CONTEXT&  fc) const
      {
          // TODO
      }
  };
  }  // close namespace bsl

Notice there are number of differences between the above formatter and one written purely for std::format, namely:

  • You need to #include the correct header:

    • #include <bsl_format.h>

  • You must partially specialize bsl::formatter (not std::formatter as many internet examples show). I.e., your formatter must be declared in the bsl namespace.

  • The constexpr keyword is required only for C++20, and may fail to compile against earlier C++ standards, so use the BSLS_KEYWORD_CONSTEXPR_CPP20 macro instead.

  • The context arguments to parse() and format() must be templated.

The actual code within the parse and format function implementations should otherwise be the same as when customizing a formatter for std::format.

Example code for a fully-fledged formatter for a user defined type

For interested readers, here is a code example for a more fully-fledged formatter for a user defined type Complex:

 #include <bsl_format.h>

/// Example of a type which we wish to format
struct Complex
{
    double d_real;
    double d_imaginary;
};

namespace bsl { // Formatter needs to be in namespace `bsl`

/// Partial specialization of `bsl::formatter` for type `Complex`
template <class t_CHAR>
struct formatter<Complex, t_CHAR> {
  private:
    // DATA
    formatter<double, t_CHAR> d_doubleFormatter;
          // Use a standard formatter to which we delegate
          // floating point formatting

    bool                      d_isJ;
          // Whether the imaginary part uses an 'i' or a 'j'

  public:
    // CREATORS
    BSLS_KEYWORD_CONSTEXPR_CPP20 formatter()
    : d_doubleFormatter()
    , d_isJ (false)
    {}

    // MANIPULATORS
    template <class t_PARSE_CONTEXT>
    typename t_PARSE_CONTEXT::iterator BSLS_KEYWORD_CONSTEXPR_CPP20 parse(t_PARSE_CONTEXT& pc)
    {
        if (pc.begin() == pc.end() || *pc.begin() == '}') {
            // Initialize the standard floating point formatter
            // If a default (empty) format specification is provided.
            return d_doubleFormatter.parse(pc);
        }
        if (*pc.begin() == 'i' || *pc.begin() == 'j') {
            // The user has specified 'i' vs 'j' for output
            if (*pc.begin() == 'j') {
                d_isJ = true;
            }
            pc.advance_to(pc.begin() + 1); // skip 'i' or 'I'
        }
        if (pc.begin() != pc.end()) {
            // Initialize the format specification with the
            // provided floating point format
            if (*pc.begin() != ':') {
                BSLS_THROW(bsl::format_error("missing separator"));
            }
            pc.advance_to(pc.begin() + 1); // skip ':'
        }
        // Update the iterator in the context and return the same.
        pc.advance_to(d_doubleFormatter.parse(pc));
        return pc.begin();
    };

    // ACCESSORS
    template <class t_FORMAT_CONTEXT>
    typename t_FORMAT_CONTEXT::iterator format(const Complex& value, t_FORMAT_CONTEXT& fc) const
    {
        typename t_FORMAT_CONTEXT::iterator out = fc.out();
        // Write out the first floating point value
        out = d_doubleFormatter.format(value.d_real, fc);
        // Write out the separator
        *out++ = (t_CHAR)'+';
        // Update the iterator in the context before passing it to the
        // floating point formatter again.
        fc.advance_to(out);
        // Write out the second floating point value
        out = d_doubleFormatter.format(value.d_imaginary, fc);
        // Write out 'i' or 'j' for the imaginary part
        *out++ = d_isJ ? (t_CHAR)'j' : (t_CHAR)'i';
        // Update the iterator in the context and return the same.
        fc.advance_to(out);
        return out;
    }
};
}  // close namespace bsl

int main()
{
    Complex c = {2.71828182845, 3.14159265359};
    bsl::cout << bsl::format("Value is {}", c) << bsl::endl;
    bsl::cout << bsl::format("Value is {0:}", c) << bsl::endl;
    bsl::cout << bsl::format("Value is {::}", c) << bsl::endl;
    bsl::cout << bsl::format("Value is {:j:}", c) << bsl::endl;
    bsl::cout << bsl::format("Value is {:j:.2f}", c) << bsl::endl;
    bsl::wcout << bsl::format(L"Value is {:j:}", c) << bsl::endl;
}
Program returned: 0
Program stdout
Value is 2.71828182845+3.14159265359i
Value is 2.71828182845+3.14159265359i
Value is 2.71828182845+3.14159265359i
Value is 2.71828182845+3.14159265359j
Value is 2.72+3.14j
Value is 2.71828182845+3.14159265359j

The examples above are tested in the unit test for bslfmt_format.

Enabling bsl::format for Types with an ostream Insert operator<<

July 15, 2025

BDE 4.26 and 4.27 introduced a couple new features that enable the use of bsl::format with types that provide ostream insert operator<<, but that do not yet (or may never) provide a formatter specialization. Currently bsl::format supports formatting of the usual suspects: fundamental types, string-like types, and even decimal floating point. Over time the range of types supported by bsl::format will expand, for example, BDE plans to introduce formatting of date and time types (based on standard formatting specifications for <chrono>).

However, there are many types that do not yet – or may never – have formatters defined for them. Many of those types will have an ostream insert operator<<, (1) because format is a comparatively new feature, and (2) implementing a formatter is more involved than implementing operator<<, and many types that do not need more complicated format specifications may forgo implementing a formatter entirely. For those types the existing operator<< will typically provide a reasonable basis for formatting.

There are a couple features (introduced in BDE 4.26 and BDE 4.27) that enable the use of bsl::format with types that provide stream based operator<< but do not yet (or may never) provide a formatter specialization.

  1. The bslfmt_streamed component provides a bslfmt::streamed function that that returns a formattable reference wrapper to a streamable object.

  2. The bslfmt_enablestreamedformatter component provides a trait that a type owner can define for a type to enable using the streaming based formatter automatically (without clients needing to use bslfmt::streamed).

bslfmt::streamed - A Wrapper to Format a Streamable Type

The bslfmt_streamed component provides the bslfmt::streamed function that is a factory taking any “streamable” type and returning a wrapper around it that is formattable:

 bsl::format("The value is {}\n", bslfmt::streamed(streamableObject));

This is particularly valuable for types you do not own because, in general, only the owners of a type should create a formatter specialization for that type.

bslfmt::streamed is a factory function that returns an instance of a bslfmt::Streamed template for the given “streamable” type, and <bslfmt_streamed.h> defines a formatter for all bslfmt::Streamed instances. bslfmt::streamed has been inspired by fmt::streamed.

Note

Using bslfmt::streamed on a type that has a formatter specialization will produce a compiler warning on modern compilers. This is because bslfmt::streamed is intended as a stop-gap for users where a formatter specialization is not (yet) available.

bslfmt::EnableStreamedFormatter - A Trait for Marking a Type Stream-Formattable

The bslfmt_enablestreamedformatter component provides the bslfmt::EnableStreamedFormatter (nested-trait-compatible) type trait that makes “streamable” types directly formattable:

 class Identifier {
     // A type that stores a string of up to 10 characters
     ...
   public:
     // TRAITS
     BSLMF_NESTED_TRAIT_DECLARATION(Identifier,
                                    bslfmt::EnableStreamedFormatter);
 };

 std::ostream& operator<<(std::ostream& os, const Identifier& obj) ...

 Identifier id("12345");

 bsl::format("The identifier is {:0>10}\n", id);

 // Will output: "The identifier is 0000012345\n"

This trait provides a short-hand for making a type formattable in situations where there is no value in providing a formatter specialization.

Note

Use this option with caution. Be aware that enabling this trait effectively promises that a particular output-format and string-like formatting options are supported for your type, which may constrain an implementation of a custom formatter in the future (that might have better performance or provide conflicting formatting options).

Format Specification for Streamed Objects

The formatting logic used to format streamable types is simple. Essentially the output of the ostream insert operator<< is treated as a (possibly UTF-8) string. This also means that objects that are formatted using either the wrapper or the trait-enabled formatter have the same format specification syntax and capabilities as string-like types (see the standard format specification for strings).

The following table shows the formatting capabilities given an imaginary object that outputs “012345” when streamed:

Width

Alignment

Pad Char

Precision

Format Spec.

Output Text

N/A

N/A

N/A

N/A

"{}"

"012345"

N/A

N/A

N/A

3

"{:.3}"

"012"

8

N/A

N/A

N/A

"{:8}"

"012345  "

8

left

N/A

N/A

"{:<8}"

"012345  "

8

center

N/A

N/A

"{:^8}"

" 012345 "

8

right

N/A

N/A

"{:>8}"

"  012345"

8

center

=

N/A

"{:=^8}"

"=012345="

6

center

*

2

"{:\*^6.2}"

"\*\*01\*\*"

Next Steps

Now that the initial release of bsl::format has been completed, the following are our immediate goals:

  • Add support for bdlt date and time types (no localization support planned)