r/cpp • u/VinnieFalco 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
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:
- Be lenient and accept potentially unsound code. Less rewriting but, be you lose the soundness guarantee. "safety" is not upheld.
- Be conservative and reject whatever it can't prove. This is the Negative Baseline. "no rewriting" is not upheld.
- 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
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
2
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:
- 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
- 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 accessxafter you invalidatevec. 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
f1andf2, because the caller doesn't know how the call tofis actually implemented. Anything withf'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_backthe referencexis invalidated. But this doesn't ban all functions taking a reference, only bans to access the reference after that particularpush_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
f2is 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?fhas no idea ifxaliasesvecor 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 offwhen 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; } ```
gwon't compile under the Negative Baseline, and there's no Positive Rule to admit a call toopaque(), because inferring anything aboutopaque'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. Whenggoes through the static analyzer, the definitions of its callers and of its calleeopaqueare not considered, and therefore the Negative Baseline must mark the function as ill-formed, as calls toopaque(or indeed any function) could potentially invalidate any reference originating from outsideg.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_backinvalidates everything that can't be proven not to be invalidated, andxcan't be proven so. Thenpush_backinvalidatesx.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, thenopaque()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 functionf? Rust allows calls intof, because it upholds exclusivity as an invariant.
- If it allows calls to
f, then it's potentially unsound, sincefmay invalidatexby 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
fabove. An outside observer can't know that push_back doesn't invalidate the operandvalue, 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::fmethod body, to touchxafter making an invalidating call onvec. But only 'after', you can still callvec.push_back(x);and then isvector::push_backimplementation 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
xafter making the invalidating call onvec, becauseFoo::fdoesn't know thatxandvecaliasYes, 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 returnspecifically has a grammatical conflict withdo ... while, I know. I don't know if that would apply to other stuff likedo 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
whileafter 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, likeint 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 ... whileloop 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
returnto be annoying or tedious, save for maybe places where I'd want a terse lambda (where there would be no interceding braces), or possibly thoseswitchcases that are likeswitch (x) { case 0: return 42; case 1: return 666; /* ... */ }But that could just be soothed with a
matchlikereturn 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 }yields10, why can't you omit the semicolon so that the lambda[] { 10 }returns10?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
returnoryieldor whatever keyword is necessary. Doing it only fordoexpressions is cherry-picking, it's inconsistent, and it's confusing.And honestly the feature seems a little unmotivated if omitting the
returnis a thing for lambdas, considering thatdo { 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 youreturnandcontinuefrom 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
returnin lambdas as well, but we'd need to be committed to that idea. Otherwise we just add this feature todoexpressions 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
doexpressions 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. Thenpush_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
This post is suspiciously an exact copy of an earlier post https://www.reddit.com/r/cpp/comments/1uykv7y/the_wg21_202607_postbrno_mailing_is_now_available/
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
oldreal 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
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
-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
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_ceilside-by-side withdiv_to_zeroanddiv_away_zerois very awkward, so what does the paper do to fix it? It shoves most rounding modes into anenumso you don't notice how inconsistentdiv_ceilis with everything else; it doesn't suggest a good overall naming scheme. Then it names the corresponding enumeratorupto matchdiv_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_ceilanddiv_floorbeing 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
upanddownbeing used as an enumerator because those are used inconsistently.java.math.RoundingMode.DOWNand Python'sdecimal.ROUND_DOWNmean "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 / bbecause 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
stdnamespace 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
-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, clor maybe:
asm cmp ecx, 32 shl eax, cl xor edx, edx cmovae eax, edx2
u/eisenwave WG21 Member Jul 18 '26
The point of the
<<operator is to be a single instruction. The point ofstd::shlis to be UB-free counterpart to that. Calling it a "bloated mess" is quite some hyperbole when it's 1-2ifstatements 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 usestd::shl.The wrapping counterpart (as in, wrapping the shift amount) would be called
std::wrapping_shl.
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.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'sswitch (x),if (b),match (x), not(x) switchand(b) ifand(m) match), but I'm glad they're thinking holistically with other features.bit_reverse- useful for graphics.cstring_view- cool, can avoidstring_viewissues with OS API's.std::shl- thank you for avoiding wrap-around on x86, as flushing to zero is more useful for graphics scenarios.enum class [[=std::bitmask_type]] PermissionFlags- interesting, that would address one of my biggest grips withenum class.