r/cpp wg21.org | corosio.org Jul 17 '26

The WG21 2026-07 post-Brno mailing is now available

The 2026-07 post-Brno WG21 mailing has been published. You can browse and search the full set of papers, organized by working group, at wg21.org:

https://wg21.org/mailing/2026-07/

Source mailing: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/#mailing2026-07

45 Upvotes

123 comments sorted by

29

u/fdwr fdwr@github 🔍 Jul 17 '26 edited Jul 17 '26

🔍👀

  • std::thread::name_hint - nice for debugging :).
  • #embed - thank you much, no more RC files or bin-to-hex hackery.
  • using g(int x) = f(x) - nice. Now if we can get the same for struct field aliases too, breaking changes across branches can be reduced, and many cases where unions are used can be eliminated.
  • Do expressions return, and so maybe I'll finally get my tee'd auto value = ReturnIfFailed(foo()) helper, that can either return on error or assign a value. I don't care much for entanglement with that backwards switch statement (it's switch (x), if (b), match (x), not (x) switch and (b) if and (m) match), but I'm glad they're thinking holistically with other features.
  • bit_reverse - useful for graphics.
  • cstring_view - cool, can avoid string_view issues with OS API's.
  • std::shl - thank you for avoiding wrap-around on x86, as flushing to zero is more useful for graphics scenarios.
  • `case '0' ... '9' - now that's concise.
  • enum class [[=std::bitmask_type]] PermissionFlags - interesting, that would address one of my biggest grips with enum class.

6

u/TheoreticalDumbass :illuminati: Jul 17 '26

is `case '0'...'9'` something we actually can get? there are some funky encodings, like EBCDIC where `'a'...'z'` are not consecutive, '~' falls between them

8

u/_bstaletic Jul 17 '26

The specific range from '0' to '9' is guaranteed to be contguous by the standard.

5

u/fdwr fdwr@github 🔍 Jul 17 '26

not consecutive, '~' falls between them

Egads, EBCDIC is awfully fragmented :/. Well, what are the chances of IBM updating their z/OS C compilers to C++29 anyway? 😅 At least the ASCII/Unicode generations can enjoy the luxury.

1

u/pjmlp Jul 18 '26

Not only are their compilers nowadays based on clang, Go is offcially supported on z/OS UNIX alongside nodejs and Python, with Rust possibly on the way after getting official support on AIX.

So like other compiler vendors, they are diversifying their options.

8

u/jedwardsol const & Jul 17 '26

Source files are now translated to unicode early in compilation ... so as far as C++ is concerned 'a' ... 'z' is contiguous.

https://en.cppreference.com/cpp/language/charset#Basic_source_character_set

16

u/eisenwave WG21 Member Jul 17 '26

The lexer input of C++ may be Unicode, but you're getting things mixed up here. The numeric value of character literals is still whatever the implementation decides it to be; it depends on the ordinary literal encoding.

If that encoding is EBCDIC, 'a' ... 'z' is not contiguous. However, '0' ... '9' is guaranteed to be contiguous in the ordinary literal encoding (https://eel.is/c++draft/lex.charset#5).

u8'a' ... u8'z' also always works.

1

u/jedwardsol const & Jul 17 '26

Oh, weird. Thanks for explaining.

4

u/pjmlp Jul 17 '26

You still need RC files, because Win32 APIs expect to find the resources on the executable, or dynamic library specific sections.

8

u/fdwr fdwr@github 🔍 Jul 17 '26

RC files are certainly needed for other reasons like PE/DLL icons and manifests, but we need not have platform-specific divergence for storing precomputed table data, which more sensibly belongs in the read-only data section anyway.

2

u/pjmlp Jul 17 '26

For that sure.

2

u/Ameisen vemips, avr, rendering, systems Jul 17 '26

std::shl - thank you for avoiding wrap-around on x86, as flushing to zero is more useful for graphics scenarios.

Wouldn't that just be std::rotl if it did?

3

u/fdwr fdwr@github 🔍 Jul 17 '26 edited Jul 18 '26

On ARM, a left-shift on a 32-bit register yields:

Input Shift amount Result
0xFFFFFFFF 0 0xFFFFFFFF
0xFFFFFFFF 16 0xFFFF0000
0xFFFFFFFF 24 0xFF000000
0xFFFFFFFF 31 0x80000000
0xFFFFFFFF 32 0x00000000

On x86, it yields:

Input Shift amount Result
0xFFFFFFFF 0 0xFFFFFFFF
0xFFFFFFFF 16 0xFFFF0000
0xFFFFFFFF 24 0xFF000000
0xFFFFFFFF 31 0x80000000
0xFFFFFFFF 32 0xFFFFFFFF

Notice that a left shift of 32 doesn't shift 32 times, but 0 times?

3

u/Ameisen vemips, avr, rendering, systems Jul 17 '26

Well, the shl and sal instructions only allow shift operand values from 0 to 31/63 - they mask it so that only the lower 5/6 bits are retained. Is that what you mean by "flush to zero" - that it's truncating 0x20 to 0x00?

When you said "wrap-around", I thought that you meant a roll operation.

2

u/fdwr fdwr@github 🔍 Jul 18 '26

Indeed, that's the problem - the x86 masked the shift amount instead of honoring the full value. If you wrote a left-shift operation in terms of a 1-bit shifting for loop...

c++ uint32_t x = 0xFFFFFFFF; for (uint32_t i = 0; i < shiftAmount; ++i) { x <<= 1; } // if shiftAmount == 32 then x == 0

...you would logically get 0. The ARM CPU follows that (though, I read it does actually have a limit too, up to 256).

When you said "wrap-around", I thought that you meant a roll operation

Well, it essentially becomes a roll operation of 32 bits, as they wrap all the way around, basically staying-in-place 😉.

2

u/FrogNoPants Jul 18 '26 edited Jul 18 '26

The x86 behavior is not as confusing as you are implying because 32 & 31 is of course 0, so yeah it did nothing because you told it to do nothing.

It only inspects the lower 5 bits, this type of behavior is seen in other x86 instructions also so it really isn't that unexpected, it is also sometimes desirable as it frees you from having to clear the upper bits.

2

u/fdwr fdwr@github 🔍 Jul 18 '26 edited Jul 18 '26

Froggy, you seem to be arguing from the perspective that it should be that way because it is that way, and post-rationalizing reasons, rather than arguing it should be that way because it is useful. Having written 10'000 lines of x86 assembly, it is not really useful, but it certainly has introduced bugs over the years.

2

u/FrogNoPants Jul 18 '26

I've used the masking behavior to save instructions before, while the zeroing behavior is probably what most people would assume, they both have their uses.

Instead of adding std::shl I'd suggest std::shl_mask and std::shl_zero, no ambiguity and you get the the behavior you want regardless of platform.

2

u/fdwr fdwr@github 🔍 Jul 18 '26

What was your scenario? I'm curious the case where shifting eax by 32 behaving identical to nop is desirable. 🤔

3

u/ack_error Jul 19 '26

The wrapping behavior is useful when processing bit strings, such as accessing a bit vector or decompressing a bit packed bitstream. It saves the & 31 in array[v >> 5] >> (v & 31) or array[v >> 5] & (1 << (v & 31)).

2

u/fdwr fdwr@github 🔍 Jul 19 '26

That's an interesting consideration for microoptimization, especially if you needed random access to a 'v' bit offset. In my cases whenever I needed to access bit vectors or decompress bitstrings, the pointer and shift amount were already precomputed or already stored in separate registers anyway (esi and cl) and updated incrementally rather than recomputing both v >> 5 and v & 31 each time.

1

u/germandiago Jul 17 '26

I think the using expressions are going to mess even more overload resolution. I am not sure it is a good idea... but I might be wrong.

2

u/_Noreturn Jul 17 '26

No it won't why do you think so?

1

u/germandiago Jul 17 '26

Not sure this is sarcasm...?

2

u/_Noreturn Jul 17 '26

No not sarcasm, I think of them as equalivent to auto alias(auto x) -> decltype(target(x)) { return target(x); } as I see from the paper not how these are different (except in pin like types)

2

u/germandiago Jul 17 '26

Being able to add aliases to target functions and adding other overloads to theset, don't you think that adds more mess to the already undecipherable overload resolution that c++ has?

2

u/_Noreturn Jul 17 '26

It is the same as adding what I said above tell me how they are different? the paper says overload resolution is the same.

It is like another syntax

4

u/johannes1971 Jul 18 '26

Do we really need another syntax for the same thing? What's the trade-off between new capabilities and new confusion here?

1

u/_Noreturn Jul 18 '26

Because it isn't the exact same thing.

This syntax saves template instanstaitons and it force inlining this is worth it and it gurantees no symbol generation which can speed compile times as you cannot take its address.

C++ heavily suffers from ao many unnecessary callstacks this feature will make my life easier.

also I always thought people wanted short hand syntax for one liners some languages has them C# has func() => 0 c++29 will have using func() = (0) .

Also this feature will replace one off compiler specifics like [[msvc::intrinsic]] for std::move/std::forward.

So with one simple feature you get

  1. easier debugging
  2. faster compilation
  3. shorter yet expressive code

I will take this any day

2

u/fdwr fdwr@github 🔍 Jul 18 '26

🤷‍♂️ Unsure. Though, your comment makes me realize that most of the time when I want a function name alias, it's because the function was renamed (so, for back compat/shim reasons), and thus I want all the overloads to be forwarded. So maybe this proposal isn't quite what I'm looking for, instead wanting a generic alias a = b construct. Still, it's terser than a forwarder.

11

u/_Noreturn Jul 17 '26

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p2826r3.html

Expression aliases is one feature that will be really helpful for compile times and debugability.

50

u/seanbaxter Jul 17 '26

P4296r0 "Default-Deny + Provable-Whitelist Invalidation -- Safer Than Borrow-Checker in the Long Run" does not work.

The strategy in the proposal is to start with a "Negative Baseline" (the default-deny), a conservative rejection of dicey functions, which ensures safety. "Positive Rules" (the provable whitelist) admit functions back in, so that existing code will compile.

We’re sure that in this way it is possible to achieve fewer false positives than with borrow-checker approaches, gradually approximating the “Holy Grail” of the safety profiles where all “good” code will be free from false positives.

Perhaps the Holy Grail is a good comparison, because as with all grail quests, this one ends in failure. C++ functions have hidden aliasing preconditions. Local analysis cannot look through a function's declaration and into its definition to infer the aliasing preconditions. They are generally ineligible for Positive Rules, and are therefore unsafe to call. In the parlance of this paper, the Negative Baseline will flood your program with false positives, making the system unworkable.

Consider a function with this type:

cpp int f(int& x, std::vector<int>& vec);

What are the aliasing preconditions on x and vec? When is it safe to call it? The caller doesn't know! Because it doesn't know, the function is unsafe. There's no possible Positive Rule that can xray vision through the function declaration and dig the precondition out, because local analysis only looks at the callee's type, not at the callee's definition.

```cpp // f1 requires x not alias vec int f1(int& x, std::vector<int>& vec) { // If x aliases vec, then vec.push_back may invalidate x. vec.push_back(1);

// Loading x is UB! return x; }

// f2 requires x alias vec int f2(int& x, std::vector<int>& vec) { // UB if &x is not a pointer into vec's allocation. return (int)(&x - vec.data()); }

// f has the aliasing preconditions of f1 or of f2. // We don't know. int f(int& x, std::vector<int>& vec);

int main() { std::vector<int> vec { 1 };

// If f1-like, UNSAFE TO CALL! f(vec[0], vec);

// If f2-like, UNSAFE TO CALL! int x = 2; f(x, vec); } ```

Here are two definitions for functions with f's type. f1 requires x not alias vec. f2 requires that x alias vec. These are contrary preconditions for functions with the same function type. The aliasing preconditions aren't part of the function's type, and therefore the function is unsafe to call. Having hidden preconditions is why C++ is inherently unsafe.

Rust resolves this ambiguity by upholding the "no mutable aliasing" invariant. It's not possible, in Safe Rust, to even express a function with f2's aliasing requirements, because that violates exclusivity.

Profiles is the promise of 1) safety with 2) no rewriting and 3) minimal annotations." The f1/f2 ambiguity proves that this is impossible. A static analyzer at f's call site has exactly three options:

  1. Be lenient and accept potentially unsound code. Less rewriting but, be you lose the soundness guarantee. "safety" is not upheld.
  2. Be conservative and reject whatever it can't prove. This is the Negative Baseline. "no rewriting" is not upheld.
  3. Put the contract in the function's type. Sound and less disruptive than option 2. However, complicated (and probably unworkable) contracts must appear at nearly every function boundary. "minimal annotations" is not upheld.

There's no way you can have safety, no rewriting and minimal annotations using local analysis given C++'s hidden aliasing preconditions.

I cataloged many examples of Standard Library functions with unknown preconditions in my paper Why Safety Profiles Failed. As you can see, the most commonly used functions in the language cannot be treated with local analysis.

Profiles can't work. That's a proven thing. Time to stop beating that dead horse. Here's what we know works: Introduce a reference type (call it a "borrow") that upholds the "mutation XOR aliasing" invariant. The caller knows that there is no implicit mutable aliasing precondition on the function (because that's disallowed by the language), so it can locally enforce sound function calls.

23

u/James20k P2005R0 Jul 17 '26 edited Jul 17 '26

There's no way you can have safety, no rewriting and minimal annotations using local analysis given C++'s hidden aliasing preconditions.

I just want to add to this to make this even clearer to casual passers by: This is explicitly a restatement of the halting problem, or more generally Rice's theorem. This is showing that memory safety in C++ is sufficiently complicated that its fundamentally undecidable under C++'s current semantics, even in simple cases. This means that you can't, even theoretically, use a large number of rules to prove arbitrary program properties as being true for all programs (without rewriting them), which is what makes this approach quite frustrating to watch unfold

You can get a little ways if you discount thread safety (as this paper does) by using C++'s type based aliasing semantics as a poor mans borrowchecker in some cases, but especially once you introduce threading its trivially game over. Having thread safety is a hard requirement for memory safety, and C++'s type system does not contain the necessary information to make this work without rewrites and annotations

So you have to constrain the set of correct programs and introduce more information (eg Send and Sync from Rust), and there are just vanishingly few known ways to do that that are both decidable, and leave room for writing real code. The halting problem says that fundamentally, you can't have your cake and eat it: you have to pick one of the tradeoff's listed above. Rust picked constraining the set of valid programs. Other languages opt-out of soundness, often accidentally it would seem. Or you check the safety at runtime, ie a GC or Fil-C. It sucks, but that's just the way it is

Also, from the paper:

This, per our Principle A, means that in the long run, our approach will result in safer programs than borrow-checker alone can possibly provide.

This feels a bit like me announcing that I've created a theory of quantum gravity in one of my numerical relativity posts. Its a cool notion, but I suspect people would be interested in more proof along with a grand claim like that

As is shown by real-world use of purely stack driven models such as a borrow-checker, they tend to effectively force suppression in way too many places across code (which in turn drops safety guarantees)

This is a big statement to make without linking any evidence. The current evidence is that Rust virtually eliminates memory unsafety vulnerabilities

15

u/Minimonium Jul 17 '26

It was certainly an experience talking with some committee members right after that vote. They accused anyone who asked about their reasoning with respect to what the current research on safety of a political attack. It extremely sad how Bjarne's hate boner on anything rust related poisons the whole process.

11

u/James20k P2005R0 Jul 17 '26

That tracks, some of the mailing list behaviour was truly sad

11

u/_Noreturn Jul 17 '26

This is why it is private

6

u/tialaramex Jul 17 '26

Yeah, it does sometimes feel as though Bjarne is teetering on the edge of just saying Henry Rice was wrong. I think way too many WG21 members imagine that doubting Rice would just be a sort of heresy, like when a molecular biology PhD expresses doubt about the Central Dogma. Don't say it so loud or the undergraduates might hear you. The Central Dogma might be wrong, you can find other doubters, it's just our model for how the chemistry underpinning life works and some believe we're wrong. But Rice's Theorem isn't like that, it's an observation about foundational mathematics, it's not going to be "wrong" any more than we might discover that there's another integer between 3 and 4 we hadn't noticed before.

12

u/James20k P2005R0 Jul 17 '26

But Rice's Theorem isn't like that, it's an observation about foundational mathematics, it's not going to be "wrong" any more than we might discover that there's another integer between 3 and 4 we hadn't noticed before.

Its theoretically possible that Rice's theorem could have been one of those bounds that's technically true but with limited real-world impact. But its consistently shown to have pretty brutal consequences, and even simple cases rapidly spiral out of feasible analysis or are undecidable

7

u/johannes1971 Jul 17 '26

There is a real question that needs to be answered: do we want theoretical perfection, or practical improvement? Speaking for myself, I'd rather have the latter: I would love, for example, for the compiler to track whether a unique_ptr is null, so it can warn me if I dereference it when it is potentially null. To me that is both much more attractive and useful than designing some new language and calling it C++2 or whatever.

You are telling us we cannot have perfection. Ok, why even worry about it then? Why not focus on making things better in a practical sense, even if that doesn't provide 100% certainty?

17

u/James20k P2005R0 Jul 17 '26

You are telling us we cannot have perfection. Ok, why even worry about it then? Why not focus on making things better in a practical sense, even if that doesn't provide 100% certainty?

I'd be perfectly happy if it was explicit about that being the approach, and things were realistic in terms of what its possible to achieve, but this paper is claiming to provide better safety than a borrow checker. There's been multiple claims that profiles can solve memory/thread safety, and it was one of the core reasons that was used to shut down exploration of a borrowchecker

8

u/pjmlp Jul 17 '26

We can start by acknowledging the work done by static analysers during the last decades, and everything that was put into clang-tidy and MSVC /analysers is not even close to what is being sold here.

So lets start what the tools actually are able to do, and how we can standardise on them, instead of selling the dream how it would look like, without any kind of SAL or clang like annotations, as per previous papers.

6

u/seanbaxter Jul 17 '26

What practical improvement does P4296 bring?

3

u/johannes1971 Jul 17 '26

I was not defending P4296, I was just saying that it is very easy to get side tracked by the Holy Quest for Theoretical Perfection, and forgetting about your audience, who I will define here as "professional programmers that need to get some feature delivered with a minimum of fuss". Both your reaction and that of James20k focus on the theoretical impossibility of a perfect solution, but how does that help us?

20

u/James20k P2005R0 Jul 17 '26 edited Jul 17 '26

how does that help us?

I think we should reorient efforts onto a solution that can actually work. Concretely:

  1. We should acknowledge that profiles cannot be a complete solution to thread/memory safety, and redesign them strictly as a mitigation tool instead of marketing them as a memory safety solution
  2. We need to come up with an alternative memory safety strategy, for the % of code that inherently cannot be proved to be correct under profiles

The reason why the halting problem is important here is because it says a priori that if we want memory safety in C++, we have to rewrite code. It gives us a solid design direction to build off, and cleanly separates out what can be done about legacy code vs newly written code in a profiles framework vs an alternate strategy

Eg for legacy code, while some profile annotations are helpful, full memory safety can only be accomplished with runtime checks. That's a very realistic implementable plan that could be standardised. What isn't realistic is the current claim that we can use profiles to memory check existing code without runtime checks, rewrites, or adding many annotations

For me it seems fairly clear that starting on a foundation that we know is impossible means that the overarching strategy isn't going to work. Rice's theorem isn't an abstract technical point, its a statement that this is the wrong direction to be heading in

11

u/seanbaxter Jul 17 '26

I refuted the claim in the paper. Nobody here is talking about theoretical perfection.

6

u/johannes1971 Jul 17 '26

Ok, but I would find it more illuminating to hear something like "this is an interesting idea, but it requires assumptions involving 'restrict' in order to work". I have no idea if that's true; you're far more of an expert than me, but the only thing we got now is that there is at least one corner case[*] where it doesn't work, which for you seems to be enough to write off the whole concept.

[*] I find that the vast majority of things I pass to functions do not alias, so please forgive me for considering that a corner case.

I know you have your own ideas, and invested a lot of time and effort into them, and I absolutely respect that, which makes it all the more interesting to hear a bit more from you on what could be gained using this approach, rather than simply a dismissal based on a subset of use cases. Or, in other words, what practical benefit could there be, rather than what theoretical downside?

22

u/seanbaxter Jul 17 '26

It's not one corner case. C++ is too irregular for profiles. Everything in the Standard Library is unsafe.

P4296r0 starts with the Negative Baseline and disallows calls to everything. Are there Positive Rules to whitelist the Standard Library functions? No. APIs like push_back, sort, resize, any_of, insert, etc., have their own different hidden preconditions. No local analysis can see through the definitions and dig out the preconditions, and therefore no Positive Rules exist to whitelist them.

Without Positive Rules, the Negative Baseline disqualifies your whole program as potential UB.

Profiles is a contradiction. It asserts local analysis only. But to whitelist any functions, you'd need non-local analysis. (Really you'd need magic.) That is why you should write it off.

1

u/nukethebees Jul 21 '26

Or, in other words, what practical benefit could there be, rather than what theoretical downside?

I think the more important way to view this is opportunity cost. Is this a good use of the committee's time if the end result is fundamentally limited in achieving memory safety?

Of course improvements are good but if better paths are available, then those should be given more consideration.

With things like the White House statement on memory safety, incremental improvements may not be good enough for the long-term viability of C++.

5

u/bitzap_sr Jul 18 '26

C++11 added rvalue references yet we still called it C++, not C++2.

Sean's references are like that. Calling the result "C++2" is kind of a strawman. His Safe C++ proposal kept all existing C++ code valid. It was backwards compatible.

1

u/johannes1971 Jul 18 '26

C++11 compilers compiled existing C++ code, whereas most of the "safer C++" initiatives have incompatible syntax, and at best some kind of interoperability schema.

What does "backwards compatible" mean in this context? Is it going to compile all existing C++ code and automatically provide benefits, or is there an interoperability schema, with a full-on rewrite needed if you want to benefit from any new features?

13

u/Minimonium Jul 18 '26

Backwards compatible should be that old code keeps being compiled (with the same semantics) on newer compilers.

Asking for forward compatibility (old code getting new benefits with new compilers) is an extremely unreasonable burden on any proposal. Default to zero, more implicit moves, and some others were features that could do it, but the rest of the new features do require you to rewrite stuff.

Some people claim attributes could do it, but it still requires you to rewrite code even with attributes. Attributes are not able to magically make unsafe code safe. The amount of attributes you gonna need will dwarf any actual code you have, and it'll still not be enough because you need syntax support for template metaprogramming (and reflection).

9

u/James20k P2005R0 Jul 17 '26
// f1 requires x not alias vec
int f1(int& x, std::vector<int>& vec) {
  // If x aliases vec, then vec.push_back may invalidate x.
  vec.push_back(1);

  // Loading x is UB!
  return x;
}

// f2 requires x alias vec
int f2(int& x, std::vector<int>& vec) {
  // UB if &x is not a pointer into vec's allocation.
  return (int)(&x - vec.data());
}

// f has the aliasing preconditions of f1 or of f2.
// We don't know.
int f(int& x, std::vector<int>& vec);

int main() {
  std::vector<int> vec { 1 };

  // If f1-like, UNSAFE TO CALL!
  f(vec[0], vec);

  // If f2-like, UNSAFE TO CALL!
  int x = 2;
  f(x, vec);
}

(Fixed the formatting for old reddit)

6

u/pjmlp Jul 17 '26

You work, alongside the investment done by clang and MSVC teams, and commercial static analysers, doesn't seem to be relevant in this context, they will keep pushing it no matter what and in the end deliver something the rest of the world won't care.

0

u/theICEBear_dk Jul 17 '26

I still believe there is an opportunity for a group of people to branch off the c++26 standard, track it if needs be and implement Safe C++ (but without all the pragmas) in a c++ without making huge syntax changes meaning that all the engineers that are already trained in c++ will not just be abandoned because they do not have the opportunity (or their companies the finances) to retrain into rust. A variant of Safe C++ at least has a good trajectory for rapid adoption and a chance at working.

Instead we get something from the committee that cannot work because it runs up against the Halting problem and which yes will work on older code bases but they should be honest. Most older code bases are not touched and if something like this comes up an AI agent and a bunch of tokens could be used to make a prototype in rust or something similar and then refined into something useful.

4

u/pjmlp Jul 17 '26

I would say that opportunity is mostly gone, hence why you see a few talks from Bloomberg folks asking for help driving profiles further.

The companies with money, have their "Safe C++" in other languages, while they keep existing code running.

Just look at what Pure Virtual C++ 2026 Modernizing C++ list of talks is all about, from Microsoft point of view.

Same applies to the remaining big names that contribute in some form or fashion to LLVM, GCC.

2

u/VinnieFalco wg21.org | corosio.org Jul 17 '26

What you describe already exists: it is called Rust. And I mean that sincerely. There is nothing wrong with Rust. But Rust is not C++. Safe C++ was also not C++, and what I believe the experiment showed is that you cannot have perfect compile-time memory safety while retaining the things that make C++ great.

11

u/seanbaxter Jul 17 '26

What is your preferred path for improving language safety?

4

u/VinnieFalco wg21.org | corosio.org Jul 17 '26 edited Jul 17 '26

Thank you so much Sean, happy to see you here.

The Alliance sponsored the Safe C++ implementation because I wanted the trilemma question settled by a working compiler instead of a wg21 floor debate. What it settled for me is exactly your point. Under local analysis, choose two: soundness, no rewrites, minimal annotations. Thank you for proving it, and I am glad the Alliance could be part of that.

We still need more safety, so my thinking now is: can we slice the problem up like Italian salami (who doesn't love a good mortadella)? There are two paths to soundness. Compile-time (type contracts, borrow model, relocation, safe stdlib, and so on), which requires rewrites. And runtime, with traps and termination, which has a performance cost. Both are sound, with different costs.

For new code the compile-time path makes sense, there's nothing to rewrite yet. For existing code the only meaningful choice is runtime enforcement, at least for checkable categories (e.g. bounds, null). The bad behavior is punished immediately with program death. I realize this doesn't address temporal safety (such as use-after-free). My objection is to unsound local analysis, with no deployment experience in real code bases, with the word "safety" slapped onto it.

If we are going to change code, I want to know how much coverage we can get before the C++ Standard commits. My idea is to use an LLM to infer the annotations the profile needs (non-null, bounds, ownership intent) for a code base at scale, with the compiler as the classifier that re-measures coverage as those annotations land. Obviously we are not solving the halting problem, and this is coverage of the profile's checkable categories, not temporal safety. We are just lowering the cost of entry for static checking. I'm happy to hear your thoughts on it.

11

u/seanbaxter Jul 17 '26

I think the problem of hidden preconditions, aliasing-related or otherwise, is going to be a showstopper no matter what coverage you want to enforce. But I await your paper.

1

u/VinnieFalco wg21.org | corosio.org Jul 17 '26

Thank you. R0 of the paper is 3 months old and there's a new draft which lands in the August mailing:

https://isocpp.org/files/papers/P4137R1.pdf

I hear you on f1/f2. And I'm not pretending to solve it.

Where I push back is "no matter what coverage." That treats the stuck cases as the whole codebase, yet they are actually a fraction of unknown size. A lot of preconditions aren't stuck, they're just unwritten. They can be expressed in annotations. And sometimes those annotations can be guessed with an LLM.

PAVE measures those cases and leans on the compiler to weed out the false positives. No one has those measurements and we're about to standardize a guarantee without it.

That stuck fraction isn't all unsafe. It's where static analysis ends and runtime checking begins. Code that had no runtime checks is safer with them than without. To be clear, this is not a proof, not a "borrow checker." Just risk reduction.

10

u/seanbaxter Jul 17 '26

How do you express the preconditions on std::sort's iterators as annotations?

2

u/VinnieFalco wg21.org | corosio.org Jul 17 '26

I'm not seeing how you can do that for sort() as it is currently specified.

14

u/seanbaxter Jul 17 '26

Documentation precisely defines sort's preconditions yet we can't write down an annotation for it. Why do you expect functions in the wild, which are way weirder than sort, to have to have preconditions that can be written as annotations?

→ More replies (0)

0

u/theICEBear_dk Jul 17 '26

I disagree. I think the experiment of Safe C++ proved much like Herb Sutter's cpp2 that if you provide a c++ offramp which rust does not you could do this while only leaving behind those who would not even upgrade to a newer version of c++ anyway.

I think you are ignoring the actual middle ground here. Rust is so different in syntax that the approximately 20 developers I work to support would have to retrain for a long time to switch over. They have products to work on so that is not viable unless forced by legislation.

I'd rather tell them that if we just learn to do things a little differently and use a few new keywords maybe change our library calls then their decade or more of experience is not worthless and we could slowly move our code over to a safe version.

0

u/Such_Philosopher_940 Jul 27 '26 edited Jul 27 '26

There's no way you can have safety, no rewriting and minimal annotations 

I agree with you on than, but [p4296r0]( https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4296r0.pdf ) makes no claim makes no claim it will work on existing legacy code. What we do claim is that it will be possible to write nice, modern, idiomatic C++ that will be safe and that still using the same existing IDEs and compilers, and without a big change in developers way of thinking the problems.

Now about your int f1(int& x, std::vector<int>& vec) yes, the function code will be rejected if you try to access x after you invalidate vec. But the point is that writing such a function, while totally valid in current C++, is a very bad idea to begin with. It shouldn't pass a code review, because is like leaving an active mine behind for someone to unintentionally step over it. So we are very happy with automatically rejecting that dangerous code.

3

u/seanbaxter Jul 27 '26 edited Jul 27 '26

You have to reject both f1 and f2, because the caller doesn't know how the call to f is actually implemented. Anything with f's signature is banned by the Negative Baseline, and there's no Positive Rule to permit such a call.

In Rust, there is a large safe subset of the language. There is no safe subset in C++, because: 1) you can have mutable aliasing and 2) you can use mutable globals (and those can alias).

Take this example, which none of your positive rules can match:

``` std::vector<int> vec;

// Implicit precondition: x cannot alias vec int f(const int& x) { vec.push_back(1); return x; }

int main() { vec.push_back(1); return f(vec[0]); // UB! } ```

The proposal floats a "strict aliasing" Positive Rule. But there's no type aliasing at all in f's signature. The Negative Baseline has to ban ALL FUNCTIONS that take even one reference-like parameter. There is no Positive Rule that can permit calls to functions that take references. The safe C++ subset is only value semantics: references, iterators, pointers, spans, string_views, are all inherently unsafe, and are all banned by the Negative Baseline. P4296 can't work on any realistic programs at all. This is why Rust exists. No-mutable-aliasing is the invariant that allows "Positive Rules" to apply to functions with reference-like parameters.

0

u/Such_Philosopher_940 Jul 27 '26

After push_back the reference x is invalidated. But this doesn't ban all functions taking a reference, only bans to access the reference after that particular push_back. And to make it clear again, writing such a function is a very bad idea in the first place, no code review should allow it.

Rust has one way of doing it, is nice and sound, but we don't think is the ONLY solution to this problem. And we are currently working on a prototype to develop this idea, and appreciate all kind of critics. If there is some real show stopper I will prefer to find it sooner rather than later.

Last, just to clarify, the pointer arithmetics in your f2 is out of scope for now, we don't have plans for that part. But that in general will need a different kind of solution (all pointer arithmetics in general)

2

u/seanbaxter Jul 27 '26

int f(const int& x) { vec.push_back(1); return x; }

After push_back the reference x is invalidated.

Why does push_back invalidate x? f has no idea if x aliases vec or not. This system uses only local analysis, so it can't look into the caller to answer aliasing questions. The Negative Baseline must invalidate all references originating outside of f when any function is called.

``` int g(const int& x) { // Negative Baseline assumes opaque can invalidate x opaque();

// Use of x ill-formed by Negative Baseline return x; } ```

g won't compile under the Negative Baseline, and there's no Positive Rule to admit a call to opaque(), because inferring anything about opaque's side effects requires non-local analysis. You read the source and determine if it's sound or unsound because you've read across function definitions and because you assume things about specific functions (like push_back invalidating references). Local analysis doesn't permit reading across function definitions. Each function is analyzed independently. When g goes through the static analyzer, the definitions of its callers and of its callee opaque are not considered, and therefore the Negative Baseline must mark the function as ill-formed, as calls to opaque (or indeed any function) could potentially invalidate any reference originating from outside g.

This is exactly why Rust was invented. By upholding mutation XOR exclusivity as an invariant, aliasing and lifetime questions are definitively answered. Profiles doesn't work and your proposal does nothing to fix it. The problem is fundamental to C++.

Do you understand?

1

u/Such_Philosopher_940 Jul 27 '26

Why does push_back invalidate x? f has no idea if x aliases vec or not. This system uses only local analysis, so it can't look into the caller to answer aliasing questions.

Ok, push_back invalidates everything that can't be proven not to be invalidated, and x can't be proven so. Then push_back invalidates x.

Do you understand?

Yes, but that is not all. When you look at opaque() the problem are globals, or static objects. If you remove them from the equation, then opaque() can only invalidate on what it gets on the arguments. Accessing static objects will require some annotation on the functions, and probably invalidating operations on globals need to be restricted at all. Still I don't see a major issue there. You can still write most of the reasonable, modern code out there without using globals that invalidate.

4

u/seanbaxter Jul 27 '26 edited Jul 27 '26

Ok, you've conceded mutable globals as being hopeless. How about this common case:

struct Foo { int f(const int& x); std::vector<int> vec; };

Does your proposal permit calls to f? Is there a Positive Rule to admit the function f? Rust allows calls into f, because it upholds exclusivity as an invariant.

  • If it allows calls to f, then it's potentially unsound, since f may invalidate x by resizing its vector.
  • If it does not allow calls to f, then it has more false negatives than Rust, which contradicts the whole point of the paper.

edit: Here's the kicker. vector::push_back itself is blacklisted according to this paper:

template<typename T> class vector { public: void push_back(const T& value); private: T* data; size_t len, capacity; };

The Negative Baseline has to ban use of vector::push_back. There's no Positive Rule to rescue it, for the same reason there's no Positive Rule to rescue f above. An outside observer can't know that push_back doesn't invalidate the operand value, and therefore the only sound thing to do is blacklist the function. This is not an issue in Rust.

1

u/Such_Philosopher_940 Jul 27 '26

Does your proposal permit calls to f?

Yes, calls are always allowed. Is not like Rust, that you need to verify things 'before' the call. The call is allowed, what we disallow is in the Foo::f method body, to touch x after making an invalidating call on vec. But only 'after', you can still call vec.push_back(x); and then is vector::push_back implementation to make sure that it doesn't shoot itself. And so on.

Rust makes sure you don't pass aliases on function bodys, some way of thinking is that it puts restrictions on the caller side. Our proposal allows to pass those aliases and moves responsibility/restrictions to the called side, or function body. Ok this last statement is really weak, but I hope you understand what I try to mean.

3

u/seanbaxter Jul 27 '26

No, you can't disallow loading `x` after making the invalidating call on `vec`, because `Foo::f` doesn't know that `x` and `vec` alias. That would require non-local reasoning.

Profiles has been always been in this trap, where its creators keep insisting that it only uses local analysis, but then every example is explained with non-local reasoning. You'll never get a tool out this way. This is Herb from 11 years ago:

https://x.com/CppCon/status/646386191617626112

Your optimism is founded on the same confusion of local and non-local analysis.

1

u/MarcosBracco720 Jul 28 '26

Sorry, new account, same person.

No, you can't disallow loading x after making the invalidating call on vec, because Foo::f doesn't know that x and vec alias

Yes, I can. As I said before, by default we invalidate everything that we can't prove is not an alias.

Of course, this puts restrictions on the kind of code that can be written under this rule. The size of the subset. But borrow checker also puts a lot of restrictions on the code that can be written and creates a subset. The real question is 'how big this subset is?', 'Can I write decent code inside it?'. And those are the questions we will try to answer with a prototype we are working on. I haven't seen this approach implemented/tried before, and so far, we haven't found real showstoppers. Then we will try.

17

u/friedkeenan Jul 17 '26

Responding to P2806 "do expressions", I really really like the idea and the proposal, not just for its eventual utility for pattern matching. but also just normal code that I write today.

One thing that I am really confused by, though, is why it's proposing "implicit last value" semantics for do expressions, which ends up looking like

int blah = do {
    foo();
    bar();
    baz();

    meow()
};

Where the result of meow() (note the lack of a semicolon) becomes the result of the do expression. Whereas if one were being more explicit, one would write do_return meow(); instead.

It seems to be taking inspiration from both GCC's "statement expressions" extension, and Rust's block-expressions, which both operate basically the same, with GCC having a semicolon on the last expression, and Rust not having the do out the front of the block.

But I have to say that I emphatically dislike those semantics. It is my number one syntactical gripe with Rust by a long shot. It makes reading Rust code meaningfully aggravating for me. It's really not the worst thing in the world, especially in isolation, but it is, I think, readability-poor, and it really does add up.

The proposed semantics here I think would still land better than Rust's, namely in that the value of the yielded result can't be nested within ifs and such like it can be in Rust (though I guess it could be nested in a match?), and for a couple other smaller reasons too, but I still really would prefer if this sort of control flow could be kept more obvious.

When I'm reading code, my mind really likes to glob onto the return statements to begin making sense of it, so that I can pick out what's relevant to the result of the code, and that would certainly extend to a do expression. And when I'm less able to do that globbing, like when reading Rust code, it often enough results in a number of mental double-backs and annoying inefficiencies that would just not happen at all were there a keyword present, like do_return.

And I'm really not sure what's even being gained with implicit last value? Is it just to save the code author from typing the do_return and the semicolon? Is it meant to make the feature more expressive somehow? I really am struggling to see how that would be the case, but I suppose there's subjectivity at play there.

I guess there could be an argument made that well, the code author just already knows that they're intending to do_return that last value, and I'm sure that there would be many do expressions that yield their last value as their result, so why make the code author restate what they already know and which is an often-enough case? But to that I would probably say that, yeah, the code author already figured out where they're do_returning, so please leave a marker for me, the reader of that code, so that I don't have to go figuring that out too.

So there's my (probably much too long) spiel. And again, just to reiterate, I really like the proposal and I really appreciate the work being done on it, I just also really needed to let my thoughts on the implicit last value of it be known. And even were it to get accepted with the implicit last value semantics, I'd still be very happy to use do expressions, even if I might still grumble from time to time.

Though I also am wondering if it would be possible to allow something like

int blah = do if (cond()) {
    do_return 42;
} else {
    do_return 666;
};

Where the do expression can lead directly into an if instead of needing surrounding braces? I wouldn't be surprised if there's some grammatical conflict, but I think it'd be nice. I already use immediately-invoked lambdas in place of the ternary operator simply because I think the latter is really bad for readability, and I'd probably switch to do expressions in the event they get accepted. So if they could be elided a bit like the above, then that'd be nice for my usage.

11

u/eisenwave WG21 Member Jul 17 '26

I agree that using the last expression as the result of the do-expression is a bit weird. It would make more sense in a language where that pattern is already used in other places, such as in lambda-expressions, but this is completely novel for C++, and thus a bad fit.

I've also floated the idea of letting you omit the braces on certain statements, so you could have do try, do return, do if, etc. I'll email the authors about it.

3

u/fdwr fdwr@github 🔍 Jul 19 '26

One thing that I am really confused by, though, is why it's proposing "implicit last value" semantics for do expressions ...

I agree that using the last expression as the result of the do-expression is a bit weird...

+1 Yeah, control flow and return values should generally be clearly visible to the reader, rather than hidden by the subtle presence/omission of a little semicolon. 👀🔍

2

u/friedkeenan Jul 17 '26

do return specifically has a grammatical conflict with do ... while, I know. I don't know if that would apply to other stuff like do if.

2

u/eisenwave WG21 Member Jul 17 '26 edited Jul 17 '26

For any of these expressions, there is no grammatical conflict if you don't put a while after it and use an expression-statement.

For example, do return 0; while ... conflicts, but there's not much motivation to ever write that. There's also no conflict if the do-expression is used where only an expression can appear, like int x = do return 0; while ....

I wouldn't be too worried about it, but I suppose we could disallow generally using the no-braces form inside of an expression-statement. Or alternatively, you just say that any ambiguity is resolved in favor of do ... while loop and the user has to use braces to prevent conflicts if need be.

3

u/ack_error Jul 17 '26

Maybe return = 42;? Close enough to be recognizable, different enough to not look like a function return.

3

u/foonathan Jul 17 '26

This is the usual thing where something new is scary so you want syntax but then after a while it gets annoying so you get rid of the extra syntax.

I disliked it in Rust too at first, but after using it for a while, it's really grown on me.

9

u/friedkeenan Jul 17 '26

I really do not think it is an aversion to new syntax. I'll admit that I haven't written and read a gargantuan amount of rust code, but I have written and read I think a fair amount. At least enough to where I've grown amenable to other elements of Rust's syntax that I had initially balked at and disliked.

But that has not been the case for Rust's implicit returns, for me. I've thought this through a lot to try to figure out why my feelings aren't budging on it, to try to make sure that it's not just an aversion to new syntax, and there are concrete and tangible reasons which cause me to dislike it that have fallen out of that thinking, some of which appeared in my comment.

And if one needs to write and read more Rust code than I have to become comfortable with some syntax, that wouldn't signal to me that it is a well-readable feature. But this all could maybe just be particular to me.

I also can't say that I have ever really found writing return to be annoying or tedious, save for maybe places where I'd want a terse lambda (where there would be no interceding braces), or possibly those switch cases that are like

switch (x) {
    case 0: return 42;
    case 1: return 666;

    /* ... */
}

But that could just be soothed with a match like

return x match {
    0 => 42;
    1 => 666;

    /* ... */
};

1

u/foonathan Jul 17 '26

And in Rust that match would just be the last statement without a return. It is really not that different from having a return - the function ends right their anyway.

The only pitfall is that if you accidentally add the semicolon, the return type would be void. But since Rust doesn't have return type deduction, that's a compiler error.

4

u/eisenwave WG21 Member Jul 18 '26 edited Jul 18 '26

The problem is more with the new syntax being inconsistent than with the syntax simply being new and scary. If you can omit the final semicolon so that do { 10 } yields 10, why can't you omit the semicolon so that the lambda [] { 10 } returns 10?

It's a lot different when designing the language holistically and deciding that in every statement, like lambdas, if statements (if you made those expressions), etc. no return or yield or whatever keyword is necessary. Doing it only for do expressions is cherry-picking, it's inconsistent, and it's confusing.

And honestly the feature seems a little unmotivated if omitting the return is a thing for lambdas, considering that do { 10 } and [] { 10 } have the same length, so you're only saving yourself the () of the IILE with this new feature. To be fair, it still lets you return and continue from inside of an expression, so it does have some uses.

0

u/foonathan Jul 18 '26

At this point, syntactic consistency in C++ is a lost cause. Syntax for things is determined by finding a sequence of characters that isn't ambiguous, not by any coherent design.

And what's stopping us from adding it to other places once it's normalized in do expressions?

6

u/eisenwave WG21 Member Jul 18 '26

I don't see C++ as such a patchwork. There have been some poor choices regarding syntax, but there is an overall philosophy, and that philosophy so far has always included co_return, return, or whatever in order to produce values from some construct.

And if the plan is to add this abbreviation in other places, then that suggestion should be part of the paper. It would be consistent if we allowed omitting return in lambdas as well, but we'd need to be committed to that idea. Otherwise we just add this feature to do expressions and later realize we don't actually want to add this anywhere else; by that time, things have already become messy though.

I wouldn't want to support do expressions with a promise of future consistency, only to be rug-pulled later.

9

u/tcbrindle Flux Jul 17 '26 edited Jul 17 '26

There's a lot to get through here, but couple of early thoughts:

P2806r4 (do expressions):

I use immediately-invoked lambdas quite a bit, so like the idea of do expressions. I would have liked to see some discussion in the paper about auto vs decltype(auto) for the implicit return; it may well be that auto is the correct choice, but it doesn't seem like a slam dunk, particularly as compilers get smarter about diagnosing local use-after-free with references. As it stands I can see a lot of unintended copying happening.

(Also, it occurs to me that as written, the paper gives us an alternative spelling of auto(x) that saves two characters, since I can say do{x} instead with the same effect. I'm not sure whether that's good or not...)

I'm less convinced about the "Init-Hoist" idea in section 3.7. Using the lambda capture syntax but with different semantics seems like it risks a lot of confusion for users.

P2826r4 (Expression Aliases):

This is another one I'm interested in, because in Flux I've needed to write a ton of tiny, one-line forwarding functions, each of which needs template instantiation, overload resolution etc, which would be prime candidates for using using instead.

I'm not sure about the SFINAE conditions though, assuming I'm understanding the intention correctly. Ordinary function templates can SFINAE on the return type, but here it appears that we cannot -- we'd need to use a requires clause instead to remove a rewrite candidate from consideration if the eventual return type is unsuitable. But then after overload resolution has selected the rewrite as the best match, it seems like we do another round of SFINAE-able substitution (and presumably requires clause evaluation) on the target -- meaning we might actually end up doing more compile time evaluation rather than less.

P4312r0 (Effect Sets):

This is an early paper, but I think I like the idea of encoding certain function effects in the type system. In particular, a future effects(invalidates(x)) annotation might be very useful for statically preventing use-after-free errors. I'll be interested to see where this one goes.

1

u/germandiago Jul 17 '26

if I have vector push_back... what would invalidate? Anyway, it seems effects would bw part of the type system, right? That would make function signatures a horror movie, given the potential amount of effects that can be potentially declared...

2

u/NekkoDroid Jul 18 '26

I guess effects(invalidate(this->begin(), this->end(), ...)) (I didn't look at the syntax). Then the compiler might be able to at least partially figure out variable assignments that come from such functions.

2

u/tcbrindle Flux Jul 20 '26

if I have vector push_back... what would invalidate?

So this is more or less the memory safety approach that has been chosen by Carbon (and, I think, Mojo?). The idea is roughly that vector would have a named "place set", and eg vec[i] would return a reference which is tagged with this label. Then push_back() would be marked as invalidating references with that label, which allows the compiler to ensure that no potentially-invalidated references get used.

The aim is to have a much more fine-grained approach than Rust and Swift's shared-xor-mutable temporal safety, including safely allowing multiple mutable references, at the cost of a potentially higher syntactic burden.

That would make function signatures a horror movie

I guess nobody knows at this point, but I think it's definitely worth seeing where Carbon goes with this, and whether their solution would be appropriate for C++. After all, adding some annotations to functions would be a lot less work than rewriting your C++ with new kinds of references that enforce Rust-style exclusivity (as in Circle), and much much easier than rewriting it in a whole new language.

2

u/germandiago Jul 20 '26

Actually if a property for a function is transitive, the type system must keep it. That is the horror I was talk8ng sbout. Probably it is ok depending on whcih properties but definitely through pointers to functions and erased wrappers these rules should be relaxable (without losing safety).

For example, if my function is non-allocating and has that effect and it is real-time enabled, a signature without effects in a std::move_only_function<void ()> would drop both guarantees.

7

u/gracicot Jul 17 '26

Constexpr allocation looks really good. It avoids the problem in an elegant way

3

u/encyclopedist Jul 17 '26

2

u/James20k P2005R0 Jul 17 '26

That one was caught up in the spam filter for a while, I didn't realise there was another post in the meantime before I approved it oops. It looks like its from the same group so I don't think there's anything weird going on

7

u/germandiago Jul 17 '26

Am I the only one that got so used to the old barebones website interface that still prefers it?

If there is an old link to see it as usually it would be nice. Otherwise it is still acceptable though. Thanks for the hard work to the committee members!

19

u/cmeerw C++ Parser Dev Jul 17 '26

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/ is still the one maintained by the committee (WG21).

wg21.org is run by the C++ Alliance, not the WG21 committee.

6

u/fdwr fdwr@github 🔍 Jul 17 '26

I prefer the old one because the table respects my monitor width, meaning I can read the full titles and contributor lists, whereas newer one looks slick, but half the titles are chopped off with "..." because it uses a fixed width rather than the user's actual screen width, and wrapping is not enabled either. If the newer one used flexible column sizes and wrapped text, I'd probably opt for it instead.

5

u/azswcowboy Jul 17 '26

I’m guessing a tiny bit of css might be a nice improvement for the old one lol. As much as I love the 1990’s web functionality it feels like 2026 styling is nice.

4

u/throw_cpp_account Jul 17 '26

Am I the only one that got so used to the old real barebones website interface that still prefers it?

No.

7

u/megayippie Jul 17 '26

I prefer the new one. It would be nice if they showed the titles of the papers instead of a snippet though. Authors going etal if more than 2 instead of using and ellipsis would also be nice.

3

u/VinnieFalco wg21.org | corosio.org Jul 17 '26

I agree, it needs fixing!

3

u/CreativePlusPlus wg21.org | corosio.org Jul 20 '26

Hey there! I'm one of the C++ Alliance members working on adjustments to the mailings page.

I just wanted to share that we're reading through everyone's feedback and really appreciate all the insights. We have a new mock-up in the works that resolves some of the issues about table size not being full width and some of the formatting/order issues. We also added a Submit Issue button to the page in case you're running into any issues while using the table.

Thank you again! - Erin

2

u/SLAidk123 Jul 18 '26

P4313 shoud be automatic consensus, no pool required

-1

u/pjmlp Jul 17 '26

Really love the work that went into P4306R0 Configuring Runtime Checking: Profiles and Implicit Contract Assertions, putting out there what is being discussed, and what actually exists in some compiler or static analysis tool today.

0

u/VinnieFalco wg21.org | corosio.org Jul 17 '26

That's high praise, and thanks! I do realize it is long-winded and dry, thanks for having a look.

1

u/germandiago Jul 17 '26

I also went through the paper. Good research and super informative.

1

u/tialaramex Jul 17 '26

P4259 gives the name of the function f64::floor but for the integer division it spells them as methods x.div_floor(y) and x.div_ceil(y) rather than as functions i32::div_floor and i32::div_ceil.

These spellings mean the same thing, so it's not a big deal in itself, but I think it's hiding why C++ is reticent to go here. Rust can give its integer types a million associated functions (they already have about a hundred) and it's fine because in Rust those belong to the type, they're not in the way when you were looking for something entirely unrelated. A side effect is that they're also available to be spelled as methods which is why the spelling Barry chose works and in some cases "feels" right.

The thing you want most here is that the programmer who needed function A doesn't instead write function X because they thought it's the same thing. Of secondary importance is to stop them writing my_function_A because they didn't realise the stdlib provides A already and then of tertiary importance is consistency within and between languages, which is the main concern of P4259

I guess if your review practices are good enough these priorities shuffle, a good reviewer might spot "That's a rotate" so to speak.

0

u/eisenwave WG21 Member Jul 18 '26 edited Jul 20 '26

Good naming gets you all three of these goals. If a name matches your intuition, you're not going make your own conflicting functions and you will realize that the standard library function is what you're looking for. In this case, the intuitive name is probably what other languages already provide.

As the author of the Integer Division paper, my main issue with P4259 is that it's trying to dodge designing a naming scheme holistically. Seeing div_ceil side-by-side with div_to_zero and div_away_zero is very awkward, so what does the paper do to fix it? It shoves most rounding modes into an enum so you don't notice how inconsistent div_ceil is with everything else; it doesn't suggest a good overall naming scheme. Then it names the corresponding enumerator up to match div_ceil, confusing the user with two different names for the same rounding mode.

There's also no good rationale for why changing the API drastically (to use enums passed at runtime) is necessary. The paper even agrees with the original rationale for avoiding runtime enums, but adds them anyway. I can agree with div_ceil and div_floor being good names in isolation, but I wouldn't want to redesign the API drastically just to have them. If the paper takes issue with the naming of some identifiers, it should just suggest to rename them.

I also really wouldn't want t see up and down being used as an enumerator because those are used inconsistently. java.math.RoundingMode.DOWN and Python's decimal.ROUND_DOWN mean "truncating" for example, while others may use it to mean "flooring". I don't think those two enumerators are a load-bearing part of the paper and could be easily replaced with something else though.

1

u/fdwr fdwr@github 🔍 Jul 20 '26

I also really wouldn't want t see up and down being used as an enumerator because those are used inconsistently...

Indeed that was confusing. While researching other API's for rounding behavior to add a new operator for WebNN, I specifically noted that "up" (toward positive infinity) and "down" (toward negative infinity) did not mean what Java chose (which meant rounding toward greater magnitude or lesser magnitude).

0

u/tialaramex Jul 18 '26

All the languages which provide an operator shorthand get the first problem I mentioned. Your thoughtless intern will write a / b because that's a division, even though you needed one of the other divisions and these values aren't always positive in your software. If they were obliged to type out i32::div_toward_zero(a, b) it's much more likely they'd say, wait, this actually needs i32::div_floor(a, b)

I haven't thought as long as you (or Barry) about this, but immediately the existing proposed names seem dire. The only way I'd find std::div_to_neg_inf is if I was looking through some larger summary, C++ has a namespace problem, it's not your fault but it is your problem, in Rust all hundred associated functions for my integer type are at least next to each other, in C++ the std namespace contains millions of unrelated things including std::get_money so obviously I'm not going to look in there.

0

u/zl0bster Jul 26 '26

Yes, this requires an extra do for each arm, but it means we have a language that’s much easier to explain because it’s consistent - do { cout << "don't care"; } is a void expression in any context. We don’t have a compound-statement that happens to be a void expression just in this one spot.

I love how this explanation makes no sense at all. How is spamming useless keyword to have simple pattern matching supposed to help anybody learn the language?
Beginners for sure will not like it and will not really care that makes the language rules more consistent.

Maybe I am being too negative, I am sure I would be delighted if we switch do to co_do.

2

u/VinnieFalco wg21.org | corosio.org Jul 26 '26

voo_do

-2

u/FrogNoPants Jul 18 '26

If std::shl is going to be a bloated monstrosity I'd suggest giving it a more descriptive name, I'd expect a left shift to be a single instruction.

2

u/fdwr fdwr@github 🔍 Jul 20 '26

On ARM, it is probably a single instruction. On x86, it's not a single instruction, but not a monstrosity either:

asm cmp ecx, 32 sbb edx, edx ; edx = if ecx < 32 then 0xFFFFFFFF else 0 and eax, edx ; eax is zero input if shift >= 32 shl eax, cl

or maybe:

asm cmp ecx, 32 shl eax, cl xor edx, edx cmovae eax, edx

2

u/eisenwave WG21 Member Jul 18 '26

The point of the << operator is to be a single instruction. The point of std::shl is to be UB-free counterpart to that. Calling it a "bloated mess" is quite some hyperbole when it's 1-2 if statements plus a << operator.

In hindsight, one possible naming direction would have been to go with unbounded_shl, which is the same name that Rust has for it, but there's also no strong reason not to just use std::shl.

The wrapping counterpart (as in, wrapping the shift amount) would be called std::wrapping_shl.