"Temporary solutions often become permanent problems." — Craig Bruce
1. Introduction
[P2216] "std::format improvements" introduced compile-time format string checks which, quoting Barry Revzin, "is a fantastic feature" ([P2757]). However, due to resource constraints it didn’t provide a good API for using formatting functions with format strings not known at compile time. As a workaround one could use type-erased API which has never been designed for that. This severely undermined safety and led to poor user experience. This paper fixes the issue by proposing direct support for runtime format strings which has been long available in the {fmt} library.
2. Problems
[P2216] "std::format improvements" introduced compile-time format string
checks for
. This obviously requires format strings be known at
compile time. However, there are some use cases where format strings are only
known at runtime, e.g. when translated through gettext ([GETTEXT]).
One possible workaround is using type-erased formatting functions such as
:
std :: string str = translate ( "The answer is {}." ); std :: string msg = std :: vformat ( str , std :: make_format_args ( 42 ));
This is not a great user experience because the type-erased API was designed to avoid template bloat and should only be used by formatting function writers and not by end users.
Such misuse of the API also introduces major safety issues illustrated in the following example:
std :: string str = "{}" ; std :: filesystem :: path path = "path/etic/experience" ; auto args = std :: make_format_args ( path . string ()); std :: string msg = std :: vformat ( str , args );
This innocent-looking code exhibits undefined behavior because format arguments store a reference to a temporary which is destroyed before use. This has been discovered and fixed in [FMT] which now rejects such code at compile time.
3. Proposal
This paper proposes adding the
function to explicitly mark
a format string as a runtime one and opt out of compile-time format string
checks.
Before | After |
---|---|
|
|
This improves usability and makes the intent more explicit. It can also enable
detection of some lifetime errors for arguments ([P2418]). This API has been
available in {fmt} since
-based format string checks were introduced
~2 years ago and usage experience was very positive. In a large codebase with
> 100k calls of
only ~0.1% use
.
This paper also proposes changing
to take lvalue references
instead of rvalue references, rejecting problematic code:
std :: filesystem :: path path = "path/etic/experience" ; auto args = std :: make_format_args ( path . string ()); // ill-formed
This has also been implemented in {fmt} catching some bugs even though the
pattern of using
has never been suggested as a way to pass
runtime format strings there. If left unchanged this will be a major safety
hole in the standard formatting facility.
In the standard itself
is already called with lvalue
references only, e.g. [format.functions]:
template < class ... Args > string format ( format_string < Args ... > fmt , Args && ... args );
Effects: Equivalent to:
return vformat ( fmt . str , make_format_args ( args ...));
Notice that there is intentionally no forwarding of
so the switch from
forwarding to lvalue references is effectively a noop there.
4. Impact on existing code
Rejecting temporaries in
is an (intentionally)
breaking change.
Searching GitHub for calls of
using the following query
"std::make_format_args" language : c ++ - path : libstdc - path : libcxx - path : include / c ++
returned only 844 results at the time of writing. For comparison, similar
search returned 165k results for
and 7.3k for
.
Such low usage is not very surprising because
is not widely
available yet.
At least 452 of these call sites use
as intended and will
require no changes:
std :: vformat_to ( std :: back_inserter ( c ), fmt . get (), std :: make_format_args ( args ...));
72 of remaining calls can be trivially fixed by removing unnecessary forwarding.
This leaves only 320 cases most of which will continue to work and the ones
that pass temporaries can be easily fixed by either switching to
or by storing a temporary in a variable.
5. Wording
Change in [format.syn]:
namespace std { ... // [format.fmt.string], class template basic_format_string template < class charT , class ... Args > struct basic_format_string ; template < class ... Args > using format_string = basic_format_string < char , type_identity_t < Args > ... > ; template < class ... Args > using wformat_string = basic_format_string < wchar_t , type_identity_t < Args > ... > ; template < class charT > struct runtime - format - string { // exposition-only basic_string_view < charT > str ; }; runtime - format - string < char > runtime_format ( string_view fmt ) { return { fmt }; } runtime - format - string < wchar_t > runtime_format ( wstring_view fmt ) { return { fmt }; } ... template < class Context = format_context , class ... Args > format - arg - store < Context , Args ... > make_format_args ( Args & & ... fmt_args ); template < class ... Args > format - arg - store < wformat_context , Args ... > make_wformat_args ( Args & & ... args ); ... }
Change in [format.fmt.string]:
namespace std { template < class charT , class ... Args > struct basic_format_string { private : basic_string_view < charT > str ; // exposition only public : template < class T > consteval basic_format_string ( const T & s ); basic_format_string ( runtime - format - string < charT > s ) : str ( s . str ) {} constexpr basic_string_view < charT > get () const noexcept { return str ; } }; }
Change in [format.arg.store]:
template < class Context = format_context , class ... Args > format - arg - store < Context , Args ... > make_format_args ( Args & & ... fmt_args );
2 Preconditions:
The type
meets the BasicFormatter requirements ([formatter.requirements]) for each
in
.
...
template < class ... Args > format - arg - store < wformat_context , Args ... > make_wformat_args ( Args & & ... args );
6. Implementation
The proposed API has been implemented in the {fmt} library ([FMT]).