A way to get python like f-strings in C++?

Humor me for a second :-) I know we don't aim to be like python, but I think we should always look to other languages for inspiration.

So python has something called f-strings, which are basically a really nice way of doing string formatting. It generally looks like this:

name = "Evan" greeting = f"Hello, {name}"

As you can see it embeds the variables that it wants to substitute from into the string itself which is very convenient. Not only that, but it is shorter, and arguably less error prone since argument misplacement will stand out more...

And we almost have this in C++20 with the addition of the fmt lib!

auto name = "Evan" auto greeting = std::format("Hello {}", name);

So what if a future version of the standard had a compiler supported transformation. Where if you wrote something like this:

f"Hello {name}"

it would be implicitly converted to this:

std::format("Hello {}", name)

The only obvious to me downside is that we'd have language feature intimately intertwined with a library feature, but there's precedent for that such as std::initializer_list.

Even things like range based for-loops and lambdas are basically the compiler saying "you wrote X, I'm going to expand it to Y for you".

I think the compiler can easily detect if you used an f-string, but forgot to include the <format> header, and simply error saying that you need to include it. In fact, I'm pretty sure I've seen such errors already.

So what does the community think? Good idea? Bad Idea?

82 comments · 85% upvoted · other discussions

comments

aocregacc

I guess it would be convenient but idk if it's necessary to have as a language feature.
What I would like to see is reflection support that's good enough to allow doing something like this as a library feature.

eteran [OP]

Well, I don't think we can do much better than std::format without some form of ability to do a transformation at a syntactic level. Maybe if we had Sutter's meta-classes or similar, but I don't think reflection alone is good enough, we'd need not only reflection, but also transformation.

aocregacc

Yeah you need some sort of injection capability, just querying things at compile time isn't enough. Maybe that doesn't fall under reflection, I thought it did.

I think if you had the ability to turn compile time strings into identifiers you could use a user defined literal and do a simple version like "hello {name}"f.If you want arbitrary expressions inside {} you would probably need more.

Edit: nevermind a UDL doesn't work here, so some more transformation capabilities are needed.

eteran [OP]

It might, I'm not an expert or any authority on what "reflection" means, I just have always considered it to be "code that can inspect itself" :shrug:

very_curious_agent

Get rid of old #macros, bring in lisp.

eteran [OP]

BTW, I agree that such a feature is not "necessary".

But I'm also a firm advocate of "if two pieces of code do the same work, but one is shorter without sacrificing clarity, then the shorter one is better".

guyinnoho

The original post content no longer exists here. The author used Redact to remove it, exercising their right to control their data & privacy.

Familiar cooing boron hurry follow provide nutty unwritten numerous

fdwr

it is shorter, and arguably less error prone

So it's not fully what you're asking for (you want brevity, and this is actually longer because it repeats the name twice), but fmt's named arguments do at least help with potential errors of mispositioning the inline arguments. e.g.

fmt::print("Hello, {name}! The answer is {number}. Goodbye, {name}.", fmt::arg("name", "World"), fmt::arg("number", 42));

OR

using namespace fmt::literals; fmt::print("Hello, {name}! The answer is {number}. Goodbye, {name}.", "name"_a="World", "number"_a=42);

eteran [OP]

Yeah , I mean it's close.

I can say that I don't think a library level solution is really possible to get exactly what I want, but this is not a bad approximation.

I'd still want the real thing though 😉

Zeer1x

"Rust can do that already" in 3...2...1...

There are proposals for C++ like this: p1819. But I doubt that there is enough interest in the standard community for things like that.

eteran [OP]

Thanks! This is interesting, though I think that that particular implementation is somewhat doomed due to unnecessary complexity and a divergent implementation from std::format, because:

  1. compile-time formatting is not entirely necessary since if you're doing formatting, you usually don't have all of the data at compile time. That's usually why you're using a formatter.

  2. if implemented in terms of std::format we get a lot "for free". That is, we get all of the existing formatting capabilities and if std::format ever is made constexpr, well, viola, you instantly get compile time f-strings too automatically.

Whole-Dot2435

I would love it it would be much simpler and better, I would definately prefer f"hello {name}" over std::format("hello {}",name). Another benefit of them is being able to write variable names directly in the text instead of passing it as function argument.

[deleted]

[removed]

eteran [OP]

This is really interesting to me because my experience is very different:

  • localization, I think you have a good point. I think that using indexed substitutions are better for that specific use case. That being said, I would never consider f-strings to be the only formatting solution. I'd say for localization, I'd recommend just using std::format directly for more control.

  • personally, I consider this:

std::print(f"Hello {name}, you have {count} dollars in your bank account named {account}");

MUCH more readable than

std::print("Hello {}, you have {} dollars in your bank account named {}", name, count, dollars);

Since I don't have to scan back and forth to see what's coming from what.

  • I haven't see this personally, in fact, as you can see from my above example, the f-string version tends to be slightly shorter in total line assuming the same variable names are used for both. Only by a slight amount.

I can see that one can choose to put parameters on new lines if there's a ton of them though. Which some might prefer.

std::print("Hello {}, you have {} dollars in your bank account named {}", name, count, dollars);

So you may have a fair point.

  • I've mixed feeling on your last point. If a review makes code better, it's a good review. But for something so trivial, i mean, yeah, it's a waste of time too. I would hope that people wouldn't do that.
TheThiefMaster

I have actually used translation systems, and named entries in the format string is best. You need to be able to reorder them according to the grammar, and it aids translation for the translators to have a hint what goes in the blank, rather than just an index

sphere991

How do you do named format strings with translation?

My understanding is that usually you might have a mechanism that turns a string like... "I have a {0} {1}" into something like "J'ai un {1} {0}". Let's just pretend this is a sensible example.

But with f-strings, what would the mechanism be to make this work? Like what would f"I have a {adj} {noun}"... do? Or does this basically have to delay lookup for the variables until runtime when you know what the format string is?

TheThiefMaster

In most translation systems the keys are English words like your example. But they aren't the names of variables from the code like they would be in fstrings - they are deliberately injected when formatting the string.

Similar to: locformat(locstring("have_text", "I have a {adj} {noun}"), {"adj", loc_item_adjective}, {"noun", loc_item_noun});

In that example, locstring takes a lookup key for the localisation system, and a fallback string (typically in English) for if it hasn't yet been translated. Locformat then takes a locstring as a format string, and substitutekey/locstring pairs to replace within that string.

Format supports something very similar with named format arguments, but it just takes any string, not a specifically localised string type.

The best localisation system I've used natively supported pluralisation of words based on a number parameter and gender of words, both choosing a gender for the inserted word and the gender of the inserted word affecting other words in the localised string: https://translator.openttd.org/static/docs/strings.html

However it used numbered parameters for rearranging inserted words so it's not the theoretical best IMO. However it's the best I have encountered, and it's open source!

Back to fstrings for a moment, they're great for things like debug logging or building argument lists for calling dependent programs or things like that that don't need localising, i.e. where the format string is known at compile time in the code. But no way should they be used for localisation directly, as it's essentially a code injection at that point!

sphere991

Thanks for the explanation!

ntrel2

The only problem is reordering the expressions if an expression has side effects.

eteran [OP]

Sure. That's just generally a bad idea overall!

TheThiefMaster

Yeah you just want basic key names in the string, not general code with expressions and function calls. That's too much power for an effectively user provided string...

ntrel2

The language could statically forbid side effects in interpolated expressions. If it can happen, it probably will somewhere.

TheThiefMaster

Unfortunately that essentially rules out any function calls.

ntrel2

Not if the language has a pure function attribute like D, or can interpret the function at compile time.

TheThiefMaster

You could indeed allow pure functions or compile time evaluated functions.

Unfortunately in C++ the former doesn't exist for runtime functions (consteval effectively guarantees it but limits the function to compile time evaluation). I'd love a pure attribute for C++

serviscope_minor

That's the same as existing formatting, always has been.

i=0;
printf("lol %i %i\n", i++, i++);
ntrel2

Your example doesn't show reordering by translators. You could have used numbered argument printf syntax, something like %2d %1d or something. But that's very rarely used. Reordering parameters is something translations probably need to do, so interpolation syntax would actually be an advantage for translators.

serviscope_minor

Your example doesn't show reordering by translators.

Indeed: my point was we've always been in the place where if you have side effects while printing, silly things can happen.

[deleted]

[deleted]

eteran [OP]

that's what we already will have...

I'm proposing a more terse syntax as seen in other languages.

IyeOnline

but there's precedent for that such as std::initializer_list.

Not to be that guy, but using std::initializer_list as an example for anything is probably not a good idea, especially when you want to argue for language-library interaction :)

range based for-loops

Are actually a better example of language-library interaction.

I think the compiler can easily detect if you used an f-string, but forgot to include the <format> header, and simply error saying that you need to include it.

Placement new without including <new>.

Good idea? Bad Idea?

Sure. This is already being discussed. See e.g. https://wg21.link/p1819

eteran [OP]

I agree. To be clear, I only reference it as a matter of precedent on a conceptual level, not as an example to strive for 😉

PhilTheQuant

The name for this generally is templating, but since C++ has existing use of that term, I'll say "string templating". It comes really from early web development worlds like Perl...

Anyway, it looks from this Stack Overflow page https://stackoverflow.com/questions/2548075/c-string-template-library like there is a project called CTemplate which provides what you're looking for.

eteran [OP]

Well, std::format already technically supplies all of basic foundation work to implement what you are calling templating. What I'm REALLY proposing is just a language level feature to give THAT better syntax.

Unfortunately, I do not believe there is any library level solution that will accomplish the goal of better syntax than what we already have with C++ 20.

RoyBellingan

check out boost.mustache, not yet official but is totally usable

nintendiator2

The only obvious to me downside is that we'd have language feature intimately intertwined with a library feature, but there's precedent for that such as std::initializer_list.

If I wanted a language where the worst things would be used as a foundation for everything else, I'd be working with PHP.

eteran [OP]

I'm not suggesting using initializer list as a "foundation". That feature itself, is obviously very flawed.

But I don't think the flaws are due to the fact that it combines a language level feature with a library level feature.

I was merely pointing out that because there is precedent for having the two things work together so closely, that I don't think other features should be dismissed out of hand on that basis.

SirLich

This thread is really hilarious.

Good idea? Bad Idea?

The programming community loves FStrings, and would like to have them everywhere. I've hacked them into Lua for example, because I wanted something similar.

The C++ community doesn't know what's good for it. Just check the comments.

eteran [OP]

This thread is really hilarious.

Yeah, it's been interesting. I don't know why so many people responded with things like "have you tried doing X?"

Where X is either to reinvent std::format badly, or to just use what we already have, and are completely missing a point of the post, that I'm suggesting a new feature 🤷‍♂️

YongLipTeh

There is a good way to circumvent this restriction with vim. (Here is something similar)

For instance, I really like this feature from f-strings

print(f"{x = }")

You will get

x = 3

You can achieve this by storing a macro, for instance

std::cout << ( x + y ) << '\n';

can be converted to

std::cout << "( x + y ) = " << ( x + y ) << '\n';

by using macros in vim, I store it in f

"f   ya(i<< ^[hhhi "^[pa = "^

This technique is quite versatile and you don't need to create any functions that slow down your code, it is exactly the same as if you wrote it yourself. Place the cursor between the parentheses and press "@f" to activate.

DavidJKay

To get feature now:

Precompile (using your own program) your private source into c++ regular source, generating "#line" in the generated source so debugging goes to your original to get feature now... best performance, but slower compiling, potentially faster than any other option.

Or have system where every object of a certain type is trackable in runtime as name/value pairs, then your "print" function can pull them up... huge performance hit but simple and fast compiling.

...

If you pre compile your own "language" to add extra features to c++, you can add a bunch more things than this to make it worth it. Eg simplify making your own classes.

DavidDinamit

We dont need it.

eteran [OP]

Technically, we don't "need" anything, the language is turning complete.

So that's not really a viable argument to not do something.

My counter argument is that it is quickly becoming basically standard in the programming community and would improve code quality and readability if we had it in C++

serviscope_minor

Technically, we don't "need" anything, the language is turning complete. So that's not really a viable argument to not do something.

I agree, though I have a vague feeling that with f-strings there is possibly an underlying and more general idea of which f-strings are a special case that could be implemented in library code along with other completely new things. Take, for example, D style (IIRC) mixins. We could do this with C++ now, I reckon, now that constexpr strings work properly. With mixins, you're very close to being able to f-strings in a library.

eteran [OP]

Perhaps, but I don't think that f-strings can be exactly implemented without a new language feature.

What I can think of are:

  1. What I proposed
  2. Reflection complete enough to bind variable names discovered while parsing the string to accessible vars.

The second is a much more involved solution but would support f-strings and more.

serviscope_minor

Perhaps, but I don't think that f-strings can be exactly implemented without a new language feature.

Oh yes completely. C++ doesn't have D style mixins, and I don't see that ever going in. BTW what they are is essentially telling the compiler "insert this string into your parse tree right here", so could be done with constexpr strings. The ergonomics would be imperfect though.

Edit: I think C++ ought to have them or enough that they can be made. It's one of those features that makes a very common case much nicer.

Kronikarz

I mean, what's wrong with doing something like, f("Hello ", name, ", you have ", count, " dollars in your bank account named ", account); ?

eteran [OP]
  1. Inflexible with regards to formatting options. There can be things like: `f"hello {var|02d}"

  2. Terrible syntax

Kronikarz

I'd argue that your second point is a matter of personal preference, but sure. Point one could easily be solved as well, but if you don't like regular C++ syntax then there's not much to be done there either :)

eteran [OP]

Sure, though I'd say that point 1 is solved by std::format, that's why I'd suggest building on it.

Point 2, absolutely a matter of opinion, but it is honestly the functional equivalent to iostreams, which most seem to consider to be a misstep in C++ design.

Meanwhile things like python f-strings exist in several languages at this point and people seem to generally like them :shrug:

[deleted]

What you are asking for is called string templates and C++21 has string templates. Read about string templates in cpp.

EDIT: It is C++20

eteran [OP]

Does it have syntax this terse? Also there is no C++21, I assume you mean C++20?

Yes, that has std:: format, I'm suggesting better syntax for that with language support.

Please read the whole post because I actually am suggesting building on the existing formatting library facilities.

[deleted]

I am not saying about std::format, i am saying string templates, exactly what you are looking for.

Also, look at this project https://github.com/OlafvdSpek/ctemplate

eteran [OP]

The templating library you linked too is very different from what I'm proposing or looking for. It's honestly not even close.

Even the simplest example has a lot of boiler plate that is needed (and it's not even clear that it can load a template from a string literal and not a file from their example). For example:

ctemplate::TemplateDictionary dict("example"); dict.SetValue("NAME", "John Smith"); dict.SetIntValue("VALUE", rand() % 100000); std::string output; // can I even load from a literal? or does it have to be a file? ctemplate::ExpandTemplate("example.tpl", ctemplate::DO_NOT_STRIP, &dict, &output); std::cout << output;

Vs what I'm talking about:

const char *name = "John Smith"; int value = rand() % 100000; std::cout << f"Hello {name}, You have just won {value}";

This is truly an apples and oranges comparison you making since the library requires literally double the amount of lines of code. The whole point of my proposal is to reduce the amount of code people need to write to output formatted strings, not increase it.

[deleted]

My bad. The solution you are looking for still doesn’t exist upto C++20. I am doing too much java these days. I was so fatigued while i was replying that I proposed the java solution to you. From Java 21 string templates are supported. But i am sure cpp has something exactly same in upcoming spec. I think i read it somewhere. Btw… sorry for confusing you. My apologies.

[deleted]

Yes i would agree with you. Just shared the link in case.

Electronicks22

Why would you want to bend yourself backwards just for the sake of syntaxe?

eteran [OP]

Isn't that pretty much the whole goal of language design?

ntrel2

cppfront supports string interpolation:

s := "hi (name)$”;
sphere991

With trailing dollar sign? I wonder why trailing.

I've seen {name}, $(name) (with parens or braces or nothing), \(name)... but this might be the first time I've seen a suffix.

ntrel2

It's because dollar means capture, and in other contexts it avoids parentheses by making it postfix: https://github.com/hsutter/cppfront/wiki/Design-note%3A-Capture

tpecholt

Cppfront is a total mess

die_liebe

Why do you think so?

ntrel2

Cppfront is empirically simpler than C++.

Ameisen

Simpler doesn't mean less messy.

DummyDDD

It sounds like you want to construct the string by concatenation, rather than by formatting, since you are inserting variables at fixed positions in a string literal. Unfortunately iostreams is kind of bad. What you want is probably a way of concatenating a string by formatting individual objects (with fmtlib primitives) into a shared buffer. You could implement such a concatenation function and it wouldn't require any language changes (but it might require some changes to fmtlib).

eteran [OP]

No, not concatenation, because it would also support everything fmt does. Including type conversions from not string types and formatting options.

DummyDDD

Fmtlib's documentation calls iostreams and it's ilk concatenative, so I would call "using fmtlib for formatting objects one at a time to shared buffer" concatenative.

It would still be an uglier solution than Python's when you are mixing positional arguments and variable references, but I think that is rarely needed since you either have a compile time-constant form, where a concatenative expression would be sufficient, or you have dynamic form, where you can't use variable references, even if there is a language extension.

eteran [OP]

But I'm not suggesting doing anything like iostream, where the arguments are sent to repeated formatters one by one. I am simply suggesting an alternative syntax for fmtlib that has support at a language level. It would support literally everything fmtlib currently supports. Just with a different syntax.

So unless you're saying that fmtlib is ALSO concatenation, I don't think what you're saying really applies here.

And if you are saying that, then I'm not sure what wouldn't qualify as "string concatenation" when formatting data into a buffer. At which point, were just talking about semantic arguments with no real difference I think.

DummyDDD

I am saying that you don't need an alternate syntax that has language level support. The cool formatting features of fmtlib can be produced by concatenation and calling it's primitives. The only feature you can't produce in a concatenation approach is reordering the arguments/referring to positional arguments, which you don't need when you have a literal format string.

eteran [OP]

I never said it was "needed", I'm saying that a better syntax for the same feature set exists, but only with language support.

This isn't a question of features, it's a question of being able to do the same thing, but with simpler, more obvious, less error prone code.

Variants of the syntax I propose exist in numerous languages to great success. The only reason I can think of why we don't have it in C++ is that C++ tends to prefer library solutions over language solutions. But this unfortunately leads to ugly syntax.

Think about how boost had "lambdas" before C++11 introduced them. Sure, language level lambdas weren't "needed", we had them in a lib... But the language level version is MUCH better.

ElHeim

But that's what Python f-strings actually do: concatenation of individually formatted substrings.

Something like:

name = "u/eteran"
res = "Hi {name}, how are you?"

is functionally equivalent to:

name = "u/eteran"
res = "Hi " + name + ", how are you?"

only with better performance because Python is using dedicated bytecode. See for example:

def fmt(name):
    return "Hi {name}, how are you?"

If we disassemble that we get:

  2           0 LOAD_CONST               1 ('Hi ')
              2 LOAD_FAST                0 (name)
              4 FORMAT_VALUE             0
              6 LOAD_CONST               2 (', how are you?')
              8 BUILD_STRING             3
             10 RETURN_VALUE

Of course you could argue that any formatting is basically the same, but...

eteran [OP]

Sure, But you can also specify custom formatters for each thing.

But even if we accept the results of all those formatters as still "concatenation", then I suppose the question is "what wouldn't be concatenation?"

In your mind what is the difference between"string formatting" and "concatenating individually formatted strings"?

I feel like this is a distinction without a difference.

ElHeim

Oh, I agree that any formatting is basically a form of concatenation (if anything, of individual characters), it's all about semantics.

Someone mentioned iostream above. If anything, f-strings can be seen as a better implementation of the same concept.

COMPUTER_WIZARD_822

Couldn't we have a string literal operator that takes str and size then some operator (maybe %) similar to ofstream operator<<, then on the structure have a string operator?

Something like string f = "hello {} {}"_fmt % "r/" % "cpp"

eteran [OP]

We could, but I wouldn't want that personally as that's, to me, not an improvement over what we already have with std::format .

That's closer to what preceded f-strings in python.

COMPUTER_WIZARD_822

I think the closest thing to what you wish is an Args... print method which uses the variable's formatter

eteran [OP]

Sure, but I'm proposing a language level new feature. Can't be don't with C++23, but maybe some future version

[deleted]

[deleted]

eteran [OP]

My suggestion is that it is literally identical to:

auto str = std::format("Hello, {}", world);

In other words, unless there is a variable named world present, it's a compiler error, and if there is one, then it will be evaluated when the expression is evaluated.

ALX23z

Unfortunately, C++ has no means to bind input variables to variables in the scope according to their names. C# and python have features available for string formatting, but once the feature is added to C++ I hope it will be useable for other situations as well.

eteran [OP]

Sure, if we had that level of reflection, we get this and MUCH more. ORMs, serialization, etc...

It's a super powerful capability when combined with other reflection features.

ALX23z

I wouldn't call that reflection, at least it is quite a few extra features parallel to reflection. Reflection is specific to inspecting data, but it should still restrict itself to data inside the function/variable declaration, not checking variables of the outside context.

eteran [OP]

Yeah, it depends on the details. Like for example, you can imagine a reflection interface that's like this, it would actually f-strings possible... if it can see into the parent's scope.

std::reflect_scope scope = std::reflect::caller(); std::reflect::value value = std::reflect::resolve_name(scope , "variable"); if (auto n = std::reflect_cast<int &>(value)) { printf("%d\n", n); } else if (auto s = std::reflect_cast<std::string &>(value)) { printf("%s\n", s.c_str()); }

or something like that, it would be very powerful.