"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 safety issue and its companion paper [P2918] proposes 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 
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. Changes since R1
- 
     Added LEWG poll results for R1. 
4. Changes since R0
- 
     Moved the API for direct runtime format string support to a separate paper [P2918] per LEWG feedback. 
- 
     Removed forwarding in print 
5. Polls
LEWG poll results for R0:
POLL: In P2905R0 split 
SF F N A SA 5 10 4 0 0 
Outcome: Strong Consensus in Favor
LEWG poll results for R1:
POLL: Send P2905R1 (Runtime Format Strings) to electronic balloting to be forwarded to library for C++26, with the intention to retroactively apply the paper to C++23.
SF F N A SA 4 7 0 0 0 
Outcome: Unanimous consent in favor.
6. Proposal
This paper proposes changing 
std :: filesystem :: path path = "path/etic/experience" ; auto args = std :: make_format_args ( path . string ()); // ill-formed 
This has been implemented in {fmt} catching some bugs even though the
pattern of using 
In the standard itself 
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 
There is forwarding in the definitions on recently added 
7. Impact on existing code
Rejecting temporaries in 
Searching GitHub for calls of 
"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 
At least 452 of these call sites use 
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 
8. Wording
Change in [format.syn]:
namespace std { ... 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.arg.store]:
template < class Context = format_context , class ... Args > format - arg - store < Context , Args ... > make_format_args ( Args & & ... fmt_args ); 
2 Preconditions:
The type 
...
template < class ... Args > format - arg - store < wformat_context , Args ... > make_wformat_args ( Args & & ... args ); 
Change in [print.fun]:
template < class ... Args > void ( FILE * stream , format_string < Args ... > fmt , Args && ... args ); 
2 Effects: If the ordinary literal encoding ([lex.charset]) is UTF-8, equivalent to:
vprint_unicode ( stream , fmt . str , make_format_args ( std :: forward < Args > ( args )... args ... )); 
Otherwise, equivalent to:
vprint_nonunicode ( stream , fmt . str , make_format_args ( std :: forward < Args > ( args )... args ... )); 
Change in [ostream.formatted.print]:
template < class ... Args > void ( ostream & os , format_string < Args ... > fmt , Args && ... args ); 
1 Effects: If the ordinary literal encoding ([lex.charset]) is UTF-8, equivalent to:
vprint_unicode ( os , fmt . str , make_format_args ( std :: forward < Args > ( args )... args ... )); 
Otherwise, equivalent to:
vprint_nonunicode ( os , fmt . str , make_format_args ( std :: forward < Args > ( args )... args ... )); 
9. Implementation
The proposed API has been implemented in the {fmt} library ([FMT]).