Use f-strings in C++! - P3412 Explained

This blog was originally written in Chinese and translated by Deepseek-v4-flash. Though I've reviewed and polished the translation, feel free to contact me if any phrasing seems unnatural.

This post explains proposal P3412R1. Since the author is Victor, I boldly speculate it will be made into C++29. If the proposal has any subsequent changes, please refer to the latest revision.

Introduction

In Python, while there are various formatting approaches:

a = 1
print("Hello, %d" % a)
print("Hello, {}".format(a))
print(f"Hello, {a}")
print(f"Hello, {a=}") # Output: Hello, a=1

But f-strings are the simplest and most intuitive. In C++, the first two approaches roughly correspond to printf and format formatting methods; P3412 aims to introduce the last two approaches provided by f-strings. For example:

int calculate(int);
// Before this proposal
std::string stringify(std::string_view prefix, int bits) {
    return std::format("{}:{}: got {} for {:#06x}", prefix, errno, 
                       calculate(bits), bits);
}
// After this proposal
std::string stringify(std::string_view prefix, int bits) {
    return f"{prefix}-{errno}: got {calculate(bits)} for {bits:#06x}";
}

Details

Implementations

In fact, f-string support directly involves the compiler. The proposal suggests the following transformation pipeline:

f"Hello, {a}";                 // #1, transformed to...
__FORMAT__("Hello, {}", (a));  // #2, transformed to...
std::format("Hello, {}", (a)); // #3

Here, #1 -> #2 is completed during the preprocessing phase, so macros written inside can also be properly expanded; #2 -> #3 is a function call, and a new global function __FORMAT__ is defined in <format>:

template<typename... Args>
std::string __FORMAT__(std::format_string<Args...> lit, Args&&... args) {
    return std::format(std::move(lit), std::forward<Args>(args)...);
}

The reason for introducing a new __FORMAT__ is to allow users to customize this function. For example, users could define the following function:

// Adds a "my: " prefix
template<typename... Args>
auto __FORMAT__(std::format_string<Args...> lit, Args&&... args)
{
    return "my: " + std::format(std::move(lit), std::forward<Args>(args)...);
}

Of course, this would conflict with the existing __FORMAT__ in <format>. You can use concepts to play a trick:

template<typename... Args>
auto __FORMAT__(std::format_string<Args...> lit, Args&&... args) requires(true) {...}

This function is constrained (even if the constraint is meaningless), while the standard library’s definition is unconstrained. In overload resolution, the former has higher priority.

Optimizations

Normally, users might want to write:

std::print(f"Hello, {a}");

However, this is actually problematic because it is equivalent to:

std::print(std::format("Hello, {}", a));

And the correct way to call it is:

std::print("Hello, {}", a);

Therefore, the proposal also introduces the x-literal, which expands directly into the above parameter form instead of replacing it with __FORMAT__:

std::print(x"Hello, {a}");

Of course, you can also use std::print("{}", f"Hello, {a}"), and in streams you can only use std::cout << f"Hello, {a}";, but since this theoretically creates a std::string, there may be additional overhead.

Side-effects

This transformation pipeline also leads to several consequences:

  • f-strings cannot be used in positions where function calls are not allowed, such as inside #include <...> or [[deprecated(...)]]. Also, since std::format is currently not constexpr, contexts like static_assert that require compile-time evaluation cannot use it either.

  • Since this happens before string literal concatenation, it can be combined with it, which looks rather peculiar. The author gave the following example: TODO

    Note that R"abc()abc" is a raw string literal, which should be relatively easy to understand. But the syntax highlighting is quite painful since it’s not supported yet.

  • Regarding cooperation with user-defined literals:

    f"value: {x}"_uc;
    

    This introduces ambiguity — does _uc decorate "value: {}" or the final formatted result? The proposal requires the latter, i.e., it expands to:

    __FORMAT__("value: {}", (x))_uc;
    

Debugging feature

Mimicking Python’s debugging feature:

x = 1
print(f"{x=}") # Output: x=1

The proposal also supports this:

f"{x=}"; // transformed to...
__FORMAT__("x={}", x);

The only ambiguity is that &MyClass::operator= also ends with =. However, pointer to member function cannot be meaningfully output, so this can be treated as an error.

Discussions

  1. A few more peculiar examples from the proposal:
f"Weird, but OK: {1 < 2, 2 > 1}";
// Transformed to:
__FORMAT__("Weird, but OK: {}", (1 < 2, 2 > 1));

int values[] = {3, 7, 1, 19, 2 };
f"Reversed: {std::set<int, std::greater<>>(values, values + 5)}";
// Transformed to:
__FORMAT__("Reversed: {}", (std::set<int, std::greater<>>(values, values + 5)));
           
f"The result is { get_result() :{width}.3}";
// Transformed to: (this is std::format's runtime width)
__FORMAT__("The result is {:{}.3}", (get_result()), (width));
  1. Why not use C++26 reflection: it can’t handle macros, can’t handle prefixes (unless we create a std::f function, which looks very weird); it would likely add more compile time; the code injection currently supported by reflection is more likely based on token sequences rather than direct strings, so forcing it would require a new feature to convert strings to token sequences.