Readit News logoReadit News
edflsafoiewq · 7 months ago
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

  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.

serbuvlad · 7 months ago
> There are two other proposals to fix these problems.

Most new features of C++ are introduced to fix problems created by previously new features added to C++.

blux · 7 months ago
This is becoming such a tiresome opinion. How are concepts fixing a problem created by previous features to the langue? What about ranges? Auto? Move semantics? Coroutines? Constexpr? Consteval? It is time for this narrative to stop.
ajb · 7 months ago
Hah. What's interesting about this is that since it doesn't require everything to actually be converted to a string, one can implement things other than just printing. So you could also implement interpretation, eg:

  pylist = python(f"[ y*{coef} for y in {pylist} if y > {threshold}]")

jeroenhd · 7 months ago
It also allow for things that will set off spidey senses in programmers everywhere despite theoretically being completely safe assuming mydb::sql() handles escaping in the format string:

   cursor = mydb::sql(f"UPDATE user SET password={password} WHERE user.id={userid}")

HeliumHydride · 7 months ago
Yes. The basic idea is that there's a specifier that allows a formatted string to transparently decay into an ordinary string (à la array-to-pointer decay) so that "auto" doesn't produce dangling references, and so that chains of more than one implicit conversion can take place.
SkiFire13 · 7 months ago
This seems pretty similar to Rust's `format_args!` macro, which however avoids these issues by being much more verbose and thus something people are less likely to use like in those examples. It does however have issues due to the abundant use of temporaries, which makes it hard to use when not immediately passed to a function. I wonder if C++'s fstrings have the same issue.
nialv7 · 7 months ago
One of the two other proposals is user defined type decay, which lets you choose what type auto will be deduced as. i.e. "auto x = y", x might not have the type of y, instead it can be anything you choose…

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!

jeroenhd · 7 months ago
> lacks the basic safety features to avoid dangling pointers

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.

thegrim33 · 7 months ago
Smart pointers were added to the language 14 years ago. You're free to use old C++ with raw pointers and manual memory management, risking dangling pointers, or use modern C++, which provides smart pointers to avoid those issues.
f1shy · 7 months ago
I would not say stop using it. But just stick to the really needed features, and stop adding more features every 3 years. Nobody can keep up, not the developers, not the compilers... is just insane.
pjmlp · 7 months ago
First we need to rewrite the likes of LLVM, GCC, V8, CUDA,... into something else.

Which is not going to happen in our lifetime, even the mighty Rust depends on LLVM for its reference implementation.

account42 · 7 months ago
C++ desperately needs a solution for wrapper types that should eagerly decay into the wrapped type unless you pass it somewhere that explicitly wants the wrapper type.
gpderetta · 7 months ago
"operator auto" has been proposed a few times. But nailing down the semantics has proven elusive.
mkoubaa · 7 months ago
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.

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.

pjmlp · 7 months ago
Because C++, just like C, Ada, Cobol, Fortran, Modula-2, Pascal is an ISO driven language.

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.

mkoubaa · 7 months ago
This is also true about

#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

pklausler · 7 months ago
> Compilers are free to do whatever they want, but then that code isn't portable.

At a practical level, this is no different from Fortran, “ISO driven” or not.

tart-lemonade · 7 months ago
If a codebase is fragile enough that libc++ changes have to be assumed breaking until proven otherwise, why take the risk? Presumably the application already has a "standard" way of formatting strings. If it ain't broke yada yada
mkoubaa · 7 months ago
It's not about assumed breaking, it's that when you upgrade libc++ you can become incompatible at runtime with your distro or any other number of libraries outside your control in ways that are difficult to detect
beeforpork · 7 months ago
It would be nice to take care to allow the use of GNU gettext() or any other convenient translation tool.

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'.

TheRealPomax · 7 months ago
The C++ language itself shouldn't be tied to any one specific application or third party tool, though. Just because they exist doesn't mean you are forced to use them, this is one of those cases where f strings don't make a lot of sense. Things with localized labels or text ideally have an id that gets looked up, so you can't do English-based string composition. Every locale gets looked up, no "just pass through the key if locale X", and lookup failures don't "still work", they result in super obvious, user-reportable nonsense.
eqvinox · 7 months ago
I guess _ could be a function that both takes and returns basic_formatted_string? (I.e. not gettext()).
mixmastamyk · 7 months ago
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.

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

snthpy · 7 months ago
PRQL contributor here:

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?

mixmastamyk · 7 months ago
I suppose simplicity and familiarity would be the primary two goals. Then congruence with SQL would be another factor specifically for PRQL.
eviks · 7 months ago
some factors:

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?

eviks · 7 months ago
Why is it odd to copy a popular and fitting alternative? What's the better one?
mixmastamyk · 7 months ago
Because it’s an variant with a wart, only picked from being backed into a corner. I mentioned shell interpolation in my previous comment.
vitaut · 7 months ago
We could introduce g-strings instead.
lifthrasiir · 7 months ago
> [...] another leading character was the only ASCII syntax left for such a thing.

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

mixmastamyk · 7 months ago
As an alternative to another leading ascii character, you offer another leading ascii character?
vitaut · 7 months ago
This proposal is not targeting C++26 and there is a new revision of it: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p34...
amluto · 7 months ago
This links to a “decays_to” proposal:

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.)

thaumasiotes · 7 months ago
> 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, 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"?

amluto · 7 months ago
A general issue with C++ (and many statically typed languages with generic) is hilariously long type names that may even be implementation details. Using auto can be a huge time saver and even necessary for some generic code. And people get in the habit of using it.
gpderetta · 7 months ago
There are many cases in C++ where the type is unspecified (std::bind for example), or even is unutterable (lambdas, or unnamed structures). I think F-strings would be an example of the latter.

You can always box of course (std::function, std::any at the limit), but it has a non-trivial cost.

edflsafoiewq · 7 months ago
IOW I believe it's the same thing as Rust's format_args! macro, but trying to get away without needing a separate format! macro by using implicit conversions.
tialaramex · 7 months ago
std::format_args! gets you a Arguments<'a> which we'll note means it has an associated lifetime.

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.

dgfitz · 7 months ago
Somehow I manage to get by just fine with c++11. I have refactored more than a few codebases that use 17 or greater.

Strangely, the codebase became more maintainable afterwards.

qalmakka · 7 months ago
C++20's concepts IMHO are a massive update over C++11. You can basically remove almost 90% of inheritance with them without incurring in any issue (you could do that earlier too, but at the expense of incredibly hard to read error messages - now that's basically solved thanks to concepts).
derriz · 7 months ago
I don't find the error messages produced by concepts much better than old school template errors. Maybe I got used to the latter with experience and definitely the compilers got better at generating useful error messages for templates as the years passed. On the other hand when I have to review code where a significant portion of the source relates to concepts, my heart sinks.

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?

3836293648 · 7 months ago
C++ concepts are a failure due to them only checking one side of the contract. And the other is basically impossible to implement without breaking other parts of the language
ryandrake · 7 months ago
Came here to post the same thing. C++11 was a major and practical step up from previous versions. I haven't seen anything in future standards that looked like it a tool I'd use day-to-day building actual production software. Much of the subsequent versions added things probably interesting to compiler and language academics. "Default constructible and assignable stateless lambdas?" Really?
vitus · 7 months ago
Off the top of my head, C++17 brought slicker notation for nested namespaces, digit separators for numeric literals (so you can more easily read 1'000'000'000), improvements in type deduction for pairs / tuples (so std::make_pair / make_tuple are basically unnecessary now), guarantees in the standard for copy elision / return value optimization in specific circumstances,. Oh, and structured bindings (so you can now write `for (const auto& [key, value] : map) { ... }`).

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).

FpUser · 7 months ago
To me moving from C++11 to 17 and then 20 was just a matter of convenience. When digging on how to do this and that I've found few things that just saved my time here and there. Also couple of valuable libs I wanted to use required newer C++ versions.
gpderetta · 7 months ago
> "Default constructible and assignable stateless lambdas?

this comes up surprisingly often.

dataflow · 7 months ago
constexpr in 11 vs 14 was night and day difference.
Laiho · 7 months ago
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.
Capricorn2481 · 7 months ago
What is this referring to? I would imagine whatever you consider recent competition is actually not that recent.
vanviegen · 7 months ago
On the time scale of c++, rust is very recent. :-)