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.
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::stringSupports 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.
iostreamscan be comparatively inefficient.iostreamscan 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:
bsl::cout << "value a (" << a << ") is less than b (" << b << ")";
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::formatwill provide support for common Bloomberg vocabary types (currentlyDecimal64, but with planned enhancements forbdltdate and time types).bsl::formatreturns absl::string(not astd::string).bsl::formatis available on all platforms from C++03 onwards (including Sun).bsl::formatis 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::formatneed to follow a few extra rules but, providing those rules are followed, they are fully compatible withstd::format.bsl::formatstring 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::formatare only available on C++20 on modern compilers.Runtime errors are reported by throwing a
bsl::format_errorrather than astd::format_error. Note, however, that on modern compilersbsl::format_erroris an alias tostd::format_errorso catchingbsl::format_errorwill work for bothbsl::formatandstd::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
#includethe correct header:#include <bsl_format.h>
You must partially specialize
bsl::formatter(notstd::formatteras many internet examples show). I.e., yourformattermust be declared in thebslnamespace.The
constexprkeyword is required only for C++20, and may fail to compile against earlier C++ standards, so use theBSLS_KEYWORD_CONSTEXPR_CPP20macro instead.The context arguments to
parse()andformat()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.
The
bslfmt_streamedcomponent provides abslfmt::streamedfunction that that returns a formattable reference wrapper to a streamable object.The
bslfmt_enablestreamedformattercomponent provides a trait that a type owner can define for a type to enable using the streaming based formatter automatically (without clients needing to usebslfmt::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
bdltdate and time types (no localization support planned)