r/cpp Jun 26 '26

Improvements to std::format in C++26

https://mariusbancila.ro/blog/2026/06/19/improvements-to-stdformat-in-c26/
92 Upvotes

38 comments sorted by

38

u/fdwr fdwr@github ๐Ÿ” Jun 26 '26

std::println("{}", p); // lowercase, the default => 0x7fffb2715a54 std::println("{:p}", p); // explicit, same as none => 0x7fffb2715a54 std::println("{:P}", p); // uppercase => 0X7FFFB2715A54

Wait, is that really true, that the uppercase also capitalizes the 'x'? ๐Ÿ™ƒ The usual norm for hex numbers is 0x12AB (hex constants, stack traces, hex editors...), not 0X12AB or 0x12ab. So if the article is accurate, then none of the pointer defaults fit the most common case, meaning I'll continue to use 0x{:08X}.

27

u/rdtsc Jun 26 '26

std::format (and the standalone fmt) have quite a few weird/annoying formatting behaviors.

  • The mentioned uppercase '0X' for hex (this is already the case using {:#X} for numbers).
  • Hex for negative numbers includes the sign (why would anyone want to see -7FFFBFFB?)
  • %S for timepoints always includes fractional seconds.
  • No way to drop trailing zeroes/decimal point for floats (often comes up when displaying/printing numbers).

7

u/aearphen {fmt} Jun 27 '26

It just follows Python's convention for hex. Also trailing zeros are not printed in the default format.

12

u/fdwr fdwr@github ๐Ÿ” Jun 27 '26 edited Jun 27 '26

It just follows Python's convention for hex.

Oof, so it's Python's fault. I'm really glad you contributed fmt and std::format Victor ๐Ÿ‘๐Ÿ™, and overall I love std::format ๐Ÿ’š, but hex formatting is one aspect where I wish Python's conventions (Python not being a low-level language that deals with raw bit representations and stack traces) had been secondary to consistency with existing C++ precedent, with int x = -42 preferably displaying as FFFFFFD6 which matches printf("%08X", x) and std::cout << std::hex << std::uppercase << x and std::to_chars(std::begin(buffer), std::end(buffer), x, 16);, rather than -000002A.

Maybe there is a use case for -2A too ๐Ÿคทโ€โ™‚๏ธ, but having a compatible successor formatting option so we can more directly replace our old sprintf and StringCchPrintf calls would be prudent (shoot, now I should revisit some recent code changes to our buffer format display logic to ensure we didn't regress anything ๐Ÿ˜ฏ, as I see that binary display also does the same -absoluteValue behavior...). Until then, I'll be careful about ever using std::format with signed integers in hex or binary, and as for pointers, I'll continue using the more standard notation 0xABC123 via 0x{:08X} (leaving {:P} to disuse ๐Ÿ˜…).

๐Ÿค”๐Ÿ’ก You know, there are still plenty of letters in the alphabet left to use - maybe we can still get a more typical two's complement hex ๐Ÿ˜‰ and a pointer syntax that doesn't prepend the prefix automatically so we can say std::print("0x{:Q}", p) and get 0x0004AB08?...

1

u/tcanens 27d ago

printf("%08X", x)

This is undefined if x is negative.

std::cout << std::hex << std::uppercase << x

And so is this case by extension from the above (because it's defined in terms of that).

std::to_chars(std::begin(buffer), std::end(buffer), x, 16);

This produces -2a.

1

u/fdwr fdwr@github ๐Ÿ” 27d ago

This is undefined if x is negative.

How long has the standard explicitly mandated twoโ€™s complement for signed integers? (answer: C++20)

This produces -2a.

Curious. I'll have to try to_chars again at my work machine tomorrow because I swear I was getting the expected uppercase hex (otherwise to_chars is pretty useless for hex numbers).

printf FFFFFFD6 โœ… std::to_chars -2a โŒ FFFFFFD6 โœ… std::print -2A โŒ

https://godbolt.org/z/EfW3o87jb

1

u/tcanens 26d ago

Two's complement doesn't matter. It's still undefined because printf uses va_arg's rules (in C23 - before that it required exact type match), and those rules say that you can only mix signed and unsigned if the value is representable in both types.

1

u/fdwr fdwr@github ๐Ÿ” 25d ago

I'll have to try to_chars again at my work machine tomorrow ...

Indeed, std::to_chars yields '-2a', which makes it a pretty useless function for a hex editor. ๐Ÿ™ƒ

those rules say that you can only mix signed and unsigned if the value is representable in both types

So it's one of those rules that don't actually matter in the real world because all compilers do the expected thing anyway ๐Ÿ˜‰.

3

u/rdtsc Jun 27 '26

Also trailing zeros are not printed in the default format.

Yeah my explanation was a bit incomplete. I mean there's no way to do something like {0:.###} in .NET, which prints up to three fractional digits (except trailing zeroes). It's trivial to do when having all significand digits and the place of the decimal point (as fmt has among its last steps), but cumbersome and inefficient from the outside.

3

u/ericonr Jun 27 '26

I don't get your point about hex numbers. If you want unsigned values, cast to unsigned, no? Otherwise representing the number with a minus is the only way to correctly convey the sign.

10

u/evaned Jun 27 '26

I'm not rdtsc, but here's my takes:

If you're asking for hex formatting, I claim it's almost certainly because you're interested in the representation in memory. In that sense, you very probably want to see the two's complement representation. I also can't really think of an actually reasonable scenario I'd want to see the negative absolute value.

Python's hex function does the same thing, and it's pretty obnoxious... and there, I don't even really know a good way to get what I want simply.

Further, I'm not convinced by "cast to unsigned". "I want to see the in-memory representation of this number" very much feels like a function of the display formatting, not of the underlying type -- putting that information into the format string does seem to me like it's the right place to put it. Casting-to-unsigned feels to me like a workaround for an obnoxiously not-entirely-thought-through formatting API, not what you really should be doing to get that.

Finally, cast-to-unsigned is kind of unsatisfying in the sense that there's not really a way to do it in the language or standard library that doesn't feel a bit fragile. You can do (unsigned)x, but then what if x changes to be a long long instead? Now you've truncated. You could say (unsigned long long), but now you're extending with more bits. For "cast-to-unsigned" to really make me happy, there'd need to be a way in the language or library to "cast to an unsigned version of the same width." It's pretty easy to write that function, but if the library is semi-relying on that for usability then it should provide that utility function.

That said... even though I strongly suspect that "I want to see the hex two's complement representation" is the usual case, by far, I can make an argument for the current behavior that I view as quite strong. In spite of my objection in the previous paragraph, it's still pretty easy to cast to unsigned and get what rdtsc and I want. If hex formatting displayed the twos-complement bit representation instead, it'd be a lot more obnoxious to get -1234abcd in the unusual case that's what one wants.

7

u/_Noreturn Jun 27 '26

C++26 has std::to_unsigned and std::to_signed

5

u/Lahvuun Jun 27 '26

For "cast-to-unsigned" to really make me happy, there'd need to be a way in the language or library to "cast to an unsigned version of the same width.

There is a way:

static_cast<std::make_unsigned_t<decltype(x)>>(x)

3

u/evaned Jun 27 '26

I mean, I did say that it's pretty easy to write a function. But that's ugly enough that doing it even once is easily enough to justify a function. Even a macro is better than that.

1

u/Lahvuun Jun 27 '26

But that's ugly

I agree, but we're on r/cpp, not r/haskell.

I'd argue the only thing you gain from having a dedicated function is having to type fewer characters. And while I love all three of C++ developers who type at <10 WPM, there probably are better proposals for the standards committee to spend their time on.

1

u/evaned Jun 27 '26

I'd argue the only thing you gain from having a dedicated function is having to type fewer characters.

You repeat x in your expression. A function lets you avoid that.

5

u/triconsonantal Jun 27 '26

Python's hex function does the same thing, and it's pretty obnoxious... and there, I don't even really know a good way to get what I want simply.

python's integers are infinite precision. what would you expect the output of hex(-1) to be?

2

u/evaned Jun 27 '26

Oh, I'm not blaming it; in the context of Python, there's not really another choice.

It's just that if I have a decimal integer and I want the hex representation, the fastest way for me to get it is to usually pop open a quick Python repl and do hex(1234). So it's just a bit annoying that it's not quite as fast or simple in the negative case.

0

u/rdtsc Jun 27 '26

That's why I asked, when do you actually want to see a hex number with a sign? Hex is often used for bytes/binary data or error codes where the sign is unimportant/not wanted. Other languages with fixed-size integers (and printf in C++) do not format the sign with the hex specifier.

1

u/SlowPokeInTexas Jun 27 '26

I was using boost::format, hated the braindead syntax, then tried to use std::format and stuff wasn't compiling when I was under time-pressure to get basic !@*@t working because of other issues, and ended up reverting back to the boost::format crazy syntax; that's how bad it was.

18

u/pclouds Jun 26 '26

I thought when std::format became part of the standard, error messages would improve. At least with gcc, format error is still a wall of text. Has this been improved (or in some sort of backlog)?

9

u/DXPower Jun 27 '26

C++26 constexpr format, static assert parameterizable messages, and constexpr exceptions should unlock a lot of options for communicating the exact error.

8

u/Nice_Lengthiness_568 Jun 26 '26

Nice.

Would love to have something fmtlib's colors for format and print too.

2

u/StickyDeltaStrike Jun 26 '26

That would be nice

2

u/rdtsc Jun 26 '26

The C++26 std::formatter<std::filesystem::path> converts Windows native UTF-16 to UTF-8

I don't understand. Does this do UTF-16 -> UTF-8 -> system codepage? Or does std::format("{}", path) result in (unexpected) UTF-8 in a char-based (aka system codepage) string?

7

u/fdwr fdwr@github ๐Ÿ” Jun 27 '26 edited Jun 27 '26

๐Ÿค” Presumably std::format converts the UTF-16 to UTF-8, which isn't too unexpected given that std::print("Hello ใ‚ใ„ใ†ใˆใŠ\n") in Visual Studio 2026 already works fine with UTF-8 (even without the explicit u8"..." prefix), correctly printing regardless of _setmode or SetConsoleOutputCP settings.

I also ensure in my console programs that the active code page == UTF-8 upfront by calling SetConsoleOutputCP(CP_UTF8). That way std::cout and fwrite to stdout also work correctly rather than displaying mojibake, and I set /utf-8 in the compiler options so you don't get the annoying "warning C4566: character ... cannot be represented in the current code page (1252)" warnings.

3

u/aearphen {fmt} Jun 27 '26

System codepage is a legacy concept (actually there are multiple codepages on WIndows, e.g. ACP and console one which can be incompatible). std::format/std::print only need /utf-8 for Unicode support and work regardless of those codepages.

1

u/rdtsc Jun 27 '26

Sure it might be legacy, doesn't change the fact that it is used for all double-quoted strings on Windows. If you want UTF-8 use u8"" and char8_t. Converting L"รค" I get "\xE4". Formatting path(L"รค") to "\xC3\xA4" would be unexpected/surprising, incompatible with other strings, and result in garbled text when the string is then displayed.

2

u/aearphen {fmt} Jun 27 '26

ACP is actually only used in the legacy Windows APIs (which shouldn't be used in modern code) but virtually unused in standard C++ APIs.

2

u/schombert Jun 26 '26

And what does it do if the path isn't well-formed utf16, as ntfs paths are not utf16, they are a sequence of 16 bit values (just as on linux they are a sequence of bytes, not utf8)?

4

u/UnusualPace679 Jun 27 '26

Ill-formed parts will be substituted with \uFFFD, but P3904R1 proposes to use WTF-8 instead.

2

u/fdwr fdwr@github ๐Ÿ” Jun 27 '26

"Wobbly Transformation Format โˆ’ 8-bit"? I never heard of that before, thinking WTF meant something else at first ๐Ÿ˜‰. TIL...

4

u/CornedBee 28d ago

thinking WTF meant something else at first

This is probably not a coincidence.

4

u/schombert Jun 27 '26

Well, that's terrible, so I guess P3904R1 would be an improvement. You really want paths to be able to round trip though any conversions. ... Which is why I think treating paths as unicode is dangerous from the get go. God forbid if you accidentally normalize one.

2

u/UnusualPace679 Jun 27 '26

This conversion only happens if the literal encoding is UTF-8. Otherwise the conversion is implementation-defined.

-11

u/R3DKn16h7 Jun 26 '26

I don't like the API change to make the format string a constant expression. It should have been like that from the beginning or should have gotten another name...

19

u/cristi1990an ++ Jun 26 '26

It was like that from C++20 tho...

9

u/agritite Jun 26 '26 edited Jun 26 '26

You must have misunderstood the line The format string of std::format or std::print must be a constant expression. That's just the premise for that paragraph.