F-strings for C++26 proposal [pdf]
open-std.org
open-std.org
std::println("Center is: {}", getCenter());
std::println(f"Center is: {getCenter()}"); // same thing, no basic_string allocated
In exchange we have the following problems // 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
There are two other proposals to fix these problems. pylist = python(f"[ y*{coef} for y in {pylist} if y > {threshold}]") cursor = mydb::sql(f"UPDATE user SET password={password} WHERE user.id={userid}")Most new features of C++ are introduced to fix problems created by previously new features added to C++.
Constexpr and auto have nothing to do with macros.
The older version is being improved, especially for ergonomics. Regarding your examples, ranges do not obsolete iterators, they are just a convenient way to pass around iterator pairs, but actual range are better implemented in terms of iterators when they are not just a composition of ranges. Similarly move semantics has little to do with nrvo (and in fact using move often is suboptimal as it inhibits nrvo).
Again, I have no idea how constexpr and auto have anything to do with macros.
Constexpr and consteval are hacks that 1) should have just been the default, and 2) shouldn't even be on the function definition, it should instead have been a keyword on the usage site: (and just use const)
int f() { ... } // any old regular function
const int x = f(); // this is always get evaluated at compile time, (or if it can't, then fail to compile)
int y = f(); // this is evaulated at runtime
That would be the sane way to do compile time functions.So in C++ "const x = EXPR" would make sense to request compile-time evaluation, but in C it wouldn't.
for (const auto item: vec) { ... }
`item` is not a compile-time constant. It's different every run of the loop.It's been so long since I used C++ for serious work that we weren't using C++11, so neither auto nor range-for were available. It would be uncommon to see "const type = " with a non-reference type and a non-constant initialiser.
Even with your example, some styles avoid "const auto item", using either "auto item" or "const auto& item" instead, because the "const" matters when taking a reference, not so much with a copy.
But I appreciate your point applies to const variables with non-constant initialisers in general, in the language.
There was once a big deal in literature about const in C++ being the "better" alternative to how #define is commonly used with C for constant values, and it seemed applicable to the thread as a key distinction between C and C++, which the parent commenter seemed to have conflated by mistake.
But I'd forgotten about const (non-reference) variables accepting non-constant initialisers, and as I hadn't used C++ seriously in a while, and the language is always changing, I checked in with a couple of C++ tutorials before writing. Unfortunately those tutorials were misleading or too simple, as both tutoruals said nothing about "const type x = " (non-reference/pointer) being uwed in any other way than for defining compile-time constants.
It's bit embarrssing, as I read other parts of the C++ standard quite often despite not using it much these days. (I'm into compiler guts, atomics, memory models, code analysis, portability issues, etc.). Yet I had forgotten this part of the language.
So, thanks for sending me down a learning & reminder rabbit-hole and correcting my error :-)
Move constructors are not needed, they don't solve a 'problem', but improve on previous semantics.
const int x = f(); // this is always get evaluated at compile time, (or if it can't, then fail to compile)
That's very silly. You're saying this should fail to compile? void foo(int x) {
const int y = bar(x);
}
There's no way the compiler can run that, because it doesn't know what x is (indeed, it would have a different value every time you run the function with a new argument). So your proposal would ditch const completely except in the constexpr case, everything runtime would have to be mutable.So you respond "well, I didn't mean THAT kind of const, you should have a different word for compile-time constants and run-time non-mutability!" Congratulations, you just invented constexpr.
There are many bad things about C++, but constexpr ain't one of them.
Yeah, I see no problem with that. Non-constant expressions usage of 'const' has always just seemed like a waste of time for me, never found it useful. But I guess a lot of people really liking typing const and "preventing themselves from accidentally mutating a variable" (when has that ever happened?), so as a compromise I guess you can have a new keyword to force constant expressions:
constexpr auto x = foo(); // always eval at compile time
const auto x = foo(); // old timey const, probably runtime but maybe got constant folded.
but it's not really a big deal what they keyword is, the main point was that "give me a constant value" should be at the usage site, not at the function definition.I can't count the number of times I've seen someone new to the language use map::operator[] without realizing that it's a mutating operation.
for (const auto &thing: collection) {
counts[thing]++;
}
Works nicely, you don't have to check if it's already there before ++ing it. As long as you know that's what operator[] does, it comes in handy more than I would've expected.Another popular use case is something along the lines of:
std::unique_ptr<T>& ptr = ptrs[key];
if (ptr != nullptr) {
ptr = std::make_unique<T>(...);
}
That said, I don't know what behavior I'd want if maps didn't automatically insert an element when the key was absent. UB (as with vector)? Throw an exception? Report the incident to Stroustrop? All these options feel differently bad.Maybe it's not a concern in C-family languages, but rust's culture of defaulting to let and only using mut when it's specifically required does feel very pleasant and ergonomic when I'm in that headspace.
The issue is, not everything can be done at compile time, and so “I can use this at compile time” becomes part of the signature because you want to ensure that it will continue to be able to be used that way. Without it, changes in your function could easily break your callers.
This isn't about "oops, I didn't mean to mutate that." This is about rapidly being able to reason about the correctness of some code that is leveraging const-qualified code.
to me, pretty much a few times per week at least
Move is fixing the problem of unnecessary mass-construction when you pass around containers.
std::ranges was introduced because dear fucking god the syntax for iterating over a partial container. (And the endless off-by-one errors)
concepts, among other things, fix (sorta) the utter shit show that templates brought to error messages, as well as debacles like SFINAE and std::enable_if.
You're right. They're not fixing problems created by previous features. They're all fixing problems created or made massively worse by templates.
Concepts fix implicit template requirements
> What about ranges?
Fix the bad iterators
> Auto
Fix the the overly long type names
> Move semantics
This is mostly necessary because of excessive copies that cpp does
> Consteval
Fix cases where constexpr couldn't do it at comptime
This is like implicit type conversion on steroids. And all this because C++ lacks the basic safety features to avoid dangling pointers.
Stop using C++ already!
And even if you could assume that pointer parameters represent borrowing, they are definitely not guaranteed to represent scoped borrowing: the function could store them somewhere, and then you end up with other issues. So shared_ptr is the only solution if you care about safety to represent a borrowed pointer. And of that's too costly, but the std designers did care about safety, they could have introduced a std::borrowed_ptr<T> that is just a wrapper around T* but that is used uniformly in all std functions that borrow a pointer and guarantee not to store it.
It doesn't. Unfortunately, C++ programmers choose not to use basic safety features for performance reasons (or aesthetics, or disagreement with the idea that a language should take into account that a programmer might make a mistake, but at least performance is a good one), but C++ actually has quite a few tricks to prevent the memory management issues that cause C/C++ bugs.
Using modern C++ safety features won't completely prevent bugs and memory issues, just like using Rust won't, but the mess that causes the worst bugs is the result of a choice, not the language itself.
Even the creator of the language admitted that "just write better code bro" approach doesn't work.
string_view, for example, is very modern and is utterly and completely unsafe.
Which is not going to happen in our lifetime, even the mighty Rust depends on LLVM for its reference implementation.
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.
Whereas Python language evolution is driven by whatever CPython reference implementation does.
Compilers are free to do whatever they want, but then that code isn't portable.
#pragma once
But it became a de facto standard at some point.
thank you for the clarification. You are 100% right about the general difference. I didn't consider the level of "confidence" python has in directing it's own evolution that I don't detect in the C++ committee
At a practical level, this is no different from Fortran, “ISO driven” or not.
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.)
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.
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'.
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
That is a valid point and something I've also been thinking about lately. I can't speak for the others but in my case the Python string interpolation syntax was the one I was most familiar with, other than bash, so it was just the default. The big idea really is to have string interpolation and the syntax is somewhat secondary but we do aim for ergonomics with PRQL so it is a consideration.
Since then I've seen more alternatives like `Hello ${var}!` in JS/TS and $"Hello {var}!" in F#. Not sure that there's a clear way to prefer one approach over the others.
What would you consider to be factors that would make you prefer one over the others?
ease of typing: so regular quotes are better vs backticks (even with a prefix), F-prefix - better than $, requiring Shift
ease of learning: here letter-mnemonic seems easiest: so I-prefix for "interpolation" or E-prefix for "expression" or maybe V-prefix for "variable". Or maybe F for "formatted" is also fine?
familiarity: so F-prefix due to Python?
Not really? The original PEP [1] for example considered `i"asdf"` as an alternative syntax. Any ASCII Latin letter besides from `b`, `r` and `u` would have been usable.
[1] https://peps.python.org/pep-0498/#how-to-denote-f-strings
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p33...
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!
So now we expect people to type:
auto s = f"{foo}";
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.But, of course, more advanced users might know what they’re doing and want to bypass this hack, so:
explicit auto s = f"{foo}";
Does what they programmer actually typed: s captures foo by reference.What could possibly go wrong?
(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 don’t compile. 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.)
Today-I-learned, Arguments<'a> has a single useful function, which appeared before I learned Rust but only very recently became usable in compile time constants, as_str() -> Option<&'static str>
format_args!("Boo!").as_str() is Some("Boo!")
If you format a literal, this always works, if you format some non-literal the compiler might realise the answer is a compile time fixed string anyway and give you that string, but it might not even if you think it should and no promises are given.
Like all shortcuts, it’s not something you can always rely on.
The whole reason for std::Arguments very existence is to call `std::Arguments::fmt` on it.
`.fmt()` is a trait implementation, but that doesn’t change anything (not sure what “kind of thing” refers to here). It’s still a function on std::Arguments.
The full name of this type is std::fmt::Arguments not std::Arguments and even so there's no such thing as std::fmt::Arguments::fmt - there is no function with that name, we can only talk about this name (since it doesn't exist) if we bring into context a specific trait such as Display or Debug
So the full name of the thing you think is the "one useful thing you can do with Arguments" is
<std::fmt::Arguments as std::fmt::Display>::fmt
or perhaps it's
<std::fmt::Arguments as std::fmt::Debug>::fmt
... as I said, Arguments implements both traits, and their sole function has the same name so we need to disambiguate somehow if we mean one of these functions or the other. For the function defined on Arguments itself, as_str, it's already unambiguous.
In the end the Debug and Display traits are all just ductwork, which is why as_str caught my attention.
explicit auto s = f"{foo}";
> Does what they programmer actually typed, so s captures foo by reference.Wouldn't this problem be best solved by... not declaring s to have a guess-what-I-mean type? If you want to be explicit about the type of s, why not just say what that type is? Wouldn't that be even more explicit than "explicit auto"?
You can always box of course (std::function, std::any at the limit), but it has a non-trivial cost.
Strangely, the codebase became more maintainable afterwards.
edit: I guess digit separators came in C++14, I'm always a little fuzzy there since at work, we jumped straight from 11 -> 17.
C++20 brought a feature that C had decades prior: designated initializers, except it's in a slightly crappier form. Also, spaceship operator (three-way comparison).
Looking at cppreference, it looks like C++17 also brought if constexpr, and standardized a bunch of nonstandard compiler extensions like [[fallthrough]]. C++20 continued standardizing more of those extensions, and also brought concepts / constraints, which are a lot easier to use than template metaprogramming.
You're at least somewhat right though -- none of these are paradigm shifts as C++11 was compared to C++03 (especially with the notion of ownership, especially in the context of std::unique_ptr and std::move).
Optional is nice but slightly awkward in a non-garbage collected language.
IMO variant is one of those things that should not exist in standard.
It tries to implement discriminated union in C++ but that feature is lame without true pattern matching. And you can’t implement pattern matching without thorough syntax level support. So in my books it’s in this academic “let’s pretend a while we are using some other language…” category.
It’s _occassionally_ convenient for sure.
I agree, they should have made it a language/syntax feature. However: if you wanna do a sum type, it does do that. I'd rather have that than nothing.
IMO the C++ standard should focus platform-agnostic functionality and leave the nitty gritty platform interaction to standalone libraries that can be patched and/or replaced as needed.
Why is it this way? As best as I can find it's so that destructors always run in reverse order of construction, I guess there could be some edge cases there that matter. It's not the strongest argument, but it's not nothing.
I have seen my share of teardown races that were fixed by reordering data members. There's nothing quite like having a mutex torn down before the object that it's guarding.
Running destructors in reverse order of construction is really the only thing that makes sense in an RAII world. It's the same argument as running the destructors in reverse order of construction when exiting a scope.
That's still not a great reason for designated initializers being restricted in the same way they were in C90, especially given the advantage of hindsight. It makes a ton of sense if you have explicit dependencies between data members created during construction, but I can't see a way that you can create those dependencies with designated initializers.
Well, consider what would happen if you had members whose value depends on other members. For example, one member is a pointer to another. Or perhaps one member uses RAII to hold a lock and another controls a resource.
Deterministic construction and destruction order is a fundamental feature of C++. Calling it clueless is just an indication one does not know C++.
That question presupposes a particular compiler implementation of designated initializers. Indeed, C90 had the fixed-order requirement until C99 decided this was unnecessary and removed it.
> Well, consider what would happen if you had members whose value depends on other members.
Can you specify a designated initializer in that way, though? Either you specify a value, or you don't; I'm not aware of a way to introduce dependencies between members with a designated initializer. Yes, you can add a default initializer to a specific member, but that only kicks in if it's unspecified by the designated initializer.
With a constructor initializer list, sure, you can absolutely introduce dependencies on previously-constructed members. But that's not the case with a designated initializer.
Much better than having yet another syntax for macros.
this comes up surprisingly often.
In my opinion, C++ "concepts" are the least useful C++20 addition to the language - awful syntax, redundancy everywhere (multiple ways of writing the same thing). And for what? Potentially better error messages?
Another gripe; of all the generic overloaded words available to describe this C++ feature, "concept" must be the least descriptive, least useful. Why pick such a meaningless name that does absolutely nothing to even suggest what the feature does?
They're not the least useful C++20 addition, in fact they're amongst the most useful ones.
In particular the addition of the "requires" expression is the real killer here.
> And for what? Potentially better error messages?
Removing even more enable_if and making template code even easier to read (you could do some of that with if constexpr + static_assert in C++17, but there were gotchas). Oh and it allows you to check for the presence of members in classes, which you couldn't do before.
That's the name that Stepanov used 30 years ago to describe the informal type constraints of templates and has been in use in the community since them. Choosing anything else for the language feature would not make sense.
Rust isn't really that unique, there are plenty of other safe languages out there. And if Graydon was alone in wanting something like Rust then Rust wouldn't have grown in popularity like it has.
Rust exists because enough people thought there was a need for Rust to exist. So if that wasn't Graydon with Rust, then it would have been someone else with something else.
This isn't meant to take anything away from Graydon nor Rust. Just saying that innovations seldom happen in silos. They're usually a result of teams of people lusting for change.
The big plus of the language was proving that Cyclone ideas to improve C, from AT&T research project were sound and could be made mainstream.
And now other languages are building on it as well, that is why Swift, Chapel, Haskell, OCaml, D are also having a go at a mix of linear types, affine types and effects.
However many folks credit Rust for type system features that are actually available in any ML derived language, or Ada/SPARK, so it isn't as if knowledge is that well spread.
Indeed. But my point is there was already widespread movement behind building a programming language. So if Mozilla hadn’t taken charge then I’m certain someone will.
My point is that Rust was born from a wider desire for change rather than that desire existing because of Rust. Thus that desire would have been met in one form or another regardless of the invention of Rust.
https://web.archive.org/web/20221206052719/https://mail.mozi...
And helped a bit when they took a lot of stuff out the stdlib into packages for the new package manager.
And helped a lot with a heavy focus pretty early on great compiler messages (inspired by elm) and with a focus on tools and documentation more generally.
Like a lot of things in life, rust was in the right place at the right time to get popular. I do think the deep want for something better and safer then c++ helped but they made a lot of good choices(not necessarily the best choices but good enough choices) and had some money backing them. I think it was far from inevitable that some other language to compete with c++ would have come out anytime soon if rust hadn't been around (and hadn't made good enough choices). It might have happened but decent chances it wouldn't have.
That's one of the reasons Rust became as widespread as it is now. However we are talking about a "what if Rust never existed" scenario. I'm confident that kind of scenario we'd be talking about a different-yet-similar language, maybe one that never got invented in our version of reality, as having the same or similar forces that helped that hypothetical language.
My point is that people wanted a successor to C++. So it was going to happen. In our reality it was Rust. But if Rust wasn't created by Greydon then someone else would have created something else to fill that void.
> I think it was far from inevitable that some other language to compete with c++ would have come out anytime soon if rust hadn't been around (and hadn't made good enough choices). It might have happened but decent chances it wouldn't have.
I very much disagree with this assumption. We have D, ObjectiveC, C#, Zig, Go, OCaml and others born out of the need to to iterate and improve on what came before it. But nothing had really caught on in the domain of safety + zero-cost abstractions principle. And particularly not aimed at C++ devs. It's been a contentious point for years -- a void people have been looking to fill. So it was only a matter of time before something caught on.
But this is all hypothetical. Plus if you subscribe to the many-worlds interpretation of quantum mechanics, then arguably we're both right :D
Could it be better? Most likely.
You could just as well say that anything beyond C89 doesn't exist, given its prevalence in some circles.
So, like I said, modules don’t exist in practice and I’d be shocked if in 2030 modules were considered normal.
C++11 was pretty game changing. C++14 and C++17 only took a few years to reach widespread adoption.
It’s very safe to require C++17 today. C++20 was a little slower and because of the modules fuckup it’s a bit inconsistent. But it’s largely fine to use.
C++23 probably needs another year or two. But also C++20 and beyond haven’t added much that’s worth upgrading for.
There are many folks that don't care though, for them it is "one platform, one compiler, language standard is whatever my compiler allows me to do, including extensions".
I am also quite bullish on the opinion that eventually, C++26 might be the last standard, not that WG21 will stop working on new ones, rather that is what many will care about when using C++ in a polyglot environment, as it is already the case in mobile OS platforms, the two major desktop platforms and distributed computing (CNCF project landscape).
Why C++26 and not earlier? Reflection.
Oh yes please! :-)
std::format is pretty nice (although not yet available on Ubuntu 24.04 LTS.
Lambda capture of parameter packs is actually huge!
And ... I think it still remains to be see what the outcome of modules will be.
One hopes (against hope) that the big payoff for modules will be in tool-ability of C++. IDE support for languages like C#, Java, typescript is vastly superior to C++ IDE tooling. Perhaps. Maybe. Modules will provide a path that will allow that to change. I don't think the benefits of modules have yet fully played out.
Visual Age for C++ v4.0 had a Smalltalk like experience with a database storage for the code, and Lucid Energize C++ already had something that people now know as LSP (Cadillac on their implementation), with incremental compilation and linking (at method/function level).
They failed commercially due to high prices and hardware requirements.
We have had C++ Builder for decades for GUI RAD development, Delphi/VB style, but due to how Borland went after the enterprise and various changes of hands, very few are aware that it exists and its capabilities.
C++ Builder with VCL was Java/.NET before these were even an idea looking for an implementation.
Problem now is that C++ has become a specialized tooling for high performance code, language runtimes, drivers and GPGPU, so you write 90% of the code in Java/C#/nodejs/..... and then reach out to native libraries, for various reasons.
Still, Clion, Visual Studio, C++ Builder, are quite good as far as development experience goes.
Do any compilers besides VS support this?
The only missing piece is that cmake is still in the process to support header units.
GCC is getting there.
An admirable statement of policy, but I'm not sure it's possible. Adding complexity to the language means there are more gotchas and edge-cases that a programmer must consider, even if they don't use the feature in question.
Since this is C++, this is not a problem we have to consider
C++ has lots of features that interact with each other in unexpected ways that could leak memory or access freed memory etc.
I imagine you never went too deep into unsafe, cross language interop, lambda evolution since the delegate days, events infrastructure, pluggable GC, RCW/CCW, JIT monitoring, the new COM replacement, how the runtime and language features differ across .NET Framework, Core, .NET MicroFramework, UWP, AOT compilation, Mono, .NET standard versus Portable Class Libraries, CLS friendly libraries,...
On top of that, all the standard frameworks that are part of a full .NET install on Visual Studio, expected that most C# developers know to at least have some passing knowledge on how to use them.
For other readers - more than half of these are irrelevant.
Writing general purpose application code rarely involves thinking about implications of most of these (save for NAOT as of lately I suppose).
Writing systems C# involves additional learning curve, but if you are already familiar with C++, it comes down to understanding the correct mapping between features, learning strengths and weaknesses of the compiler and the GC and maybe doing a cursory disassembly check now and then, if you care about it.
They simply do not matter. For example - CLS-compatiblity, seriously? I'd return the favour and ask the interviewer why they disagree with the .NET's team stance that this lost relevance in early .NET versions more than a decade ago.
There are main framework and features to be aware of, there are some that may be relevant to legacy codebases you must avoid like fire, and there are those to which the only appropriate response would be "this never existed, if it did, forget about it".
(to Pjmlp - please do not equate knowing the terms with understanding them, and stop bringing up whatever was left by wayside of history to people who should have no business being bothered by this nonsense, thank you)
I do whatever I please, feel free to ignore my comments, downvote them, or whatever goes on your heart regarding them.
And while I expect any junior not to know half of them, anyone claiming to be a senior better have an answer, regardless of what I throw at them.
Naturally I don't expect anyone versed in desktop frameworks to master backend and vice-versa, but they better know the bits that relate to desktop in that case, across the whole stack.
I like this feature as string formatting is something frequently used and this certainly looks cleaner and quicker to write.
Rusts string formatting machinery does not require any heap allocations at all, you can for example impl fmt::Write for a struct that writes directly to a serial console byte-by-byte with no allocations, and then you have access to all of rusts string formatting features available to print over a serial console!
I'm not sure about the horrifying and dangerous extensions part though, I'm not really a C++ expert so I don't know if there's a better way to do what they want to do.
Formatting on the foreground thread would be a non-starter.
fmt library can also do something similar, but still requires the complexity of adding the library and passing arguments.
Personally, I'd much prefer a smaller and more stable language.
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
"%d things I hate about f-strings\n" % (int(many()))
modes of thinking.Ironic I guess?
Look up Swift strings. Python has 4 types of string literals. Swift has 1. And they are BETTER and more powerful. Cleaner.
So compared to Python, string interpolation is always "on" and doesn't need an f-prefix. Because it uses the string escaping syntax, it doesn't have to take over a regular character like {, which requires {{ escaping.
There are two errors you could make in Python. Accidentally using {} in a normal string where you wanted interpolation, and accidentally using {} in an f-string where you wanted literal {}. I definitely do the former a lot.
And you can do this:
####"foo ###\(this is not interpolation) ####\(but this is) bar"####
In Pythons f-strings you can't ever write the literal `{foo}` in a string for documentation for example. It's a mess.
Swift doesn't have raw strings and normal strings. Swift has ONE syntax:
{#n}"string {#n}\(interpolation)"{#*n}
Where n can be zero or more. So let's recap:
1. ONE rule for start/end/escape prefix: A number of # that has to match. 2. ONE rule for escape: \.
I do Python full time and I wouldn't consider switching to Swift, but the string situation is horrible compared to Swift.
There is always a clean escape where you can write the literal that you want to write. Unlike in Python where there is no escape (amusingly).
I tried to warn the D guys not to make this exact mistake, but they didn't listen :/
You might be on your own here.
To quote myself:
Yea, f-strings are nice. But f-strings, r-strings, \ escaping, {{ escaping, ''' strings? Horrible.
Swift strings have ONE string. Just a single clean design that does all of that. With a simple set of rules.
Swift has just 1 string.
That doesn't automatically mean it's a good idea in C++, knowing C++ there are gonna be a whole lot of gotchas which aren't in Python, but it means that, at least in my opinion, how F-strings worked in Python is an argument in favor of them rather than against them.
Swift strings have ONE string. Just a single clean design that does all of that. With a simple set of rules.
Seriously, I just keep being amazed that people are running with the idea of having a full-blown untyped and unchecked formatting mini language (that's what libfmt, which became C++20 format, literally calls it) inside string literals — i.e., the part of the source code that you're specifically telling the compiler to not treat as code.
Think about it for a minute.