So the f-string literal produces a basic_formatted_string, which is basically a reified argument list for std::format, instead of a basic_string. This allows eg. println to be overloaded to operate on basic_formatted_string without allocating an intermediate string<p><pre><code> std::println("Center is: {}", getCenter());
std::println(f"Center is: {getCenter()}"); // same thing, no basic_string allocated
</code></pre>
In exchange we have the following problems<p><pre><code> // f-strings have unexpected type when using auto or type deduction.
// basic_string is expected here, but we get basic_formatted_string.
// This is especially bad because basic_formatted_string can contain
// dangling references.
auto s = f"Center is: {getCenter()}";
// f-strings won't work in places where providing a string currently
// works by using implicit conversion. For example, filesystem methods
// take paths. Providing a string is okay, since it will be implicitly
// converted to a path, but an f-string would require two implicit
// conversions, first to a string, then to path.
std::filesystem::exists(f"file{n}.dat"); // error, no matching overload
</code></pre>
There are two other proposals to fix these problems.
Tangent: this sort of thing can be implemented without any change to libc++ (the runtime). Updates to compiler versions are sometimes postponed by users with big codebases that treat a libc++ change as something major.<p>Why don't we see gcc or clang or msvc back porting stuff like this to an older version with a sort of future tag. It's normal to see __future__ in the python ecosystem, for instance.
It would be nice to take care to allow the use of GNU gettext() or any other convenient translation tool.<p>Recap: _("foo %s") macroexpands to gettext("foo %s"), then "foo %s" is extracted to a lexicon of strings by an external tool, which can be translated and compiled into .po files, which are loaded at runtime so gettext() can use a translated string based on $LC_MESSAGES. (And there is also _N(..) for correct plural handling.)<p>To do this with f-strings, _(f"foo {name()}") (which is a bit ugly...) needs to translate to make_formatted_string(_("foo {}"), name()) -- note that the _(...) needs to be called before calling make_formatted_string, to be able to return a translated string.<p>I would wish for a proposal for f-strings to consider translating strings, because we live in a world with many languages. And maybe cite gettext as a convenient method, and think about what could be done. Or point to a better tool. Or state: 'in that case, f-strings cannot be used'.
So, the f-string in Python is "spelled" that way because another leading character was the only ASCII syntax left for such a thing. It's odd that PRQL and now potentially C++ might copy it. In the PRQL case it was a new thing so they could have chosen anything, double quotes (like shell interpolation) or even backticks, that seem to make more sense.<p>Also the f- prefix was supposed to be short for format and pronounced that way. But "eff" caught on and now devs the world over are calling them "eff strings" ... funny. :-D
This proposal is not targeting C++26 and there is a new revision of it: <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3412r1.pdf" rel="nofollow">https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p34...</a>
This links to a “decays_to” proposal:<p><a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3398r0.pdf" rel="nofollow">https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p33...</a><p>And observes that this additional feature is needed to avoid dangling references. And, as a long time C++ programmer, this illustrates one of the things I dislike most about C++. In most languages, if you make a little mistake involving mixing up something that references something else with something that contains a copy, you end up with potential overhead or maybe accidental mutation. In Rust, you get a compiler error. In C++, you get use-after-free, and the code often even seems to work!<p>So now we expect people to type:<p><pre><code> auto s = f"{foo}";
</code></pre>
And those people expect s to act like a string. But the designers (reasonably!) do not want f to unconditionally produce an actual std::string for efficiency reasons, so there’s a proposal to allow f to produce a reference-like type (that’s a class value, not actually a reference), but for s to actually be std::string.<p>But, of course, more advanced users might know what they’re doing and want to bypass this hack, so:<p><pre><code> explicit auto s = f"{foo}";
</code></pre>
Does what they programmer actually typed: s captures foo by reference.<p>What could possibly go wrong?<p>(Rust IMO gets this exactly right: shared xor mutable means plus disallowing code that would be undefined behavior means that the cases like this where the code might do the wrong thing <i>don’t compile</i>. Critically, none of this actually strictly requires Rust’s approach to memory management, although a GC’d version might end up with (deterministic) runtime errors instead unless some extra work is done to have stronger static checking. And I think other languages should learn from this.)
Somehow I manage to get by just fine with c++11. I have refactored more than a few codebases that use 17 or greater.<p>Strangely, the codebase became more maintainable afterwards.
Is it a coincidence that all these quality life things start to pop up after C++ is facing real competition for the first time? Seems a bit odd to add print after using std::out for 30 years.
Like the compile time isn't long enough. They should just let the compiler do the job like Wformat=2 and skip any preprocess and constexpr function.
I'm pretty sure boost::format can do this, though not inline in the string. Do we really need more complexity in cpp? isn't it complex enough?
Making any changes to the core language is a sensitive thing as it inevitably imposes new demands on compilers, a learning curve for all users of the language, and risks breaking compatibility and introducing unforeseen issues that will need to be fixed with future changes to the language.<p>Personally, I'd much prefer a smaller and more stable language.
I am tired of PDFs. They should have a dedicated website for presenting C++ proposals so everyone can comment and discuss. Reading Github issues are more enjoyable than reading PDFs.
I agree that we should have safe-by-default "decay" behavior to a plain ol std::string, but I'm also picking up that many aren't certain it's a useful syntactic sugar in top of the fmt lib? Many other languages have this same syntax and it quickly becomes your go-to way to concatenate variables into a string. Even if it didn't handle utf-8 out of the box, so what? The amount of utility is still worth it.
I'm going to make an asinine prediction. We will be exploring F-strings in future languages in 100 years time, encountering the same problems and questions.<p>I still use printf semantics in Python3 despite trying to get with the program for symbolic string/template logic. I don't need to be told it's better, I need some Philip-K-Dick level brain re-wiring not to reach for<p><pre><code> "%d things I hate about f-strings\n" % (int(many()))
</code></pre>
modes of thinking.
When I saw the title I thought “F-strings” might be some novel variant of P—strings. I was disappointed that this is just about formatting. I really would prefer safer string handling in modern C/++
Yeah, I really missed ubiquitous C preprocessor macros in C++, so let's bring them back, but now inside string literals. Sweet.<p>Seriously, I just keep being amazed that people are running with the idea of having a full-blown untyped and unchecked <i>formatting mini language</i> (that's what libfmt, which became C++20 format, literally calls it) inside <i>string literals</i> — i.e., the part of the source code that you're specifically telling the compiler to <i>not treat as code</i>.<p>Think about it for a minute.