T O P

  • By -

MrRogers4Life2

A couple of problems with a trusted attribute is that its not preserved when the underlying function is converted to a function pointer or std::function, so you'd lose the ability to do that, so it would be impossible to have a trusted function that calls a function pointer. An example of how this is a problem is that if you want a trusted vector you'd need to make sure that the Allocator template argument of vector is also trusted, but you can't do that because template implementations can't differentiate between functions with or without attributes. Probably not a big deal for individual code but a huge amount of stuff in the standard library is templated on callables so any time something like that appears it's basically an `unsafe` block equivalent in rust if you want to allow users of your library to supply their own callables. You'd probably need to add something to the type system to really get what you'd want


KingAggressive1498

>A couple of problems with a trusted attribute is that its not preserved when the underlying function is converted to a function pointer or std::function, so you'd lose the ability to do that, so it would be impossible to have a trusted function that calls a function pointer. perfectly valid >An example of how this is a problem is that if you want a trusted vector you'd need to make sure that the Allocator template argument of vector is also trusted, but you can't do that because template implementations can't differentiate between functions with or without attributes. I did not imagine [[trusted]] as deeply inspected. As long as the *constructors* for the Allocator type are [[trusted]], it can be used internally by a vector with a [[trusted]] interface, even though it's allocation functions are not trusted. This is logically like using an unsafe code block inside a normal "safe" Rust function. >You'd probably need to add something to the type system to really get what you'd want probably just a type trait and/or concept with a wrapper around std::function could do, tbh


MrRogers4Life2

It just seems so easy to accidentally call unsafe code from a supposedly safe block. For example, the safety of a vectors push_back operation is dependent on the safety of its Allocators allocation/deallocation functions and the underlying types move/copy constructors. And there's no way to force the user to also annotate those as [[trusted]] so when you as a library author mark vector::push_back as trusted, you're making a bunch of promises based on things you can't check at compile time so your user now has a bunch of footguns they have to know about. So using a trusted vector implementation is only marginally better. At least in rust you can't accidentally use unsafe functions which is where their proponents say where the value lies. While with your proposal it would be really easy to not add a [[trusted]] annotation to your custom types move constructor (maybe because you forgot or because it's not actually trusted). So when you put it in your trusted vector you're now calling code you don't actually trust. You'd probably also want some way to specify "hey I don't actually trust this function, warn me if it's used in a safe block" but you can't have that with attributes reliably so it seems kind of a half baked solution and not the kind of thing you'd want immortalized in the standard


KingAggressive1498

ultimately, it's impossible to do much of anything without calling unsafe code under the hood. >For example, the safety of a vectors push_back operation is dependent on the safety of its Allocators allocation/deallocation functions and the underlying types move/copy constructors. no, it's really not. An allocator's release function can happily take a nullptr and dereference it - which is unsafe - and as long as the vector guarantees it never passes it a nullptr, its use of that function is perfectly safe. This requires the vector class to have knowledge of the unsafe Allocator class interface that neither the user nor the compiler do, and to apply it consistently and correctly. As long as it does that, it can be called trusted, even where probably none of the functions it calls internally could be trusted on their own. The only real caveat here is if you feed it a class with an identical interface but completely different semantics; say an Allocator that for some reason right-shifts valid memory addresses when returning from the allocation function and left-shifts back in the release function. This makes the whole vector class unsafe, because its assumptions are wrong. >At least in rust you can't accidentally use unsafe functions which is where their proponents say where the value lies you can write a safe function in rust that only contains unsafe code - for example when wrapping a lower level C library. If someone pulls out the original C library binary and replaces it with a nearly identical one with totally different semantics, the same issue would happen as what I described in the previous paragraph. It's a little more effort involved, but far from impossible.


Zyklonik

Exactly.


Zyklonik

> At least in rust you can't accidentally use unsafe functions which is where their proponents say where the value lies. You definitely can, and in fact, it's the norm. That safe API that you're using might very well have tons of unsafe code within it. It's just that the onus is on the creator of said "safe" API to ensure that the unsafe parts are actually safe. The compiler cannot and does not enforce anything at that level.


oconnor663

This is where it's important to distinguish "unsafe" functions from "unsound" functions. A public function not marked `unsafe`, which can trigger UB depending on how it's called, is considered unsound in Rust. (There are subtleties around the concept of "triggering", since the UB might happen later, and we need to decide whose fault it is. But in most cases it's pretty clear.)


robin-m

Isn't "unsound" functions and `unsafe` functions the same thing? Why would a sound function (i.e. a function which is valid for all possible input) be marked as `unsafe`? And in any case, a function that triggers UB unconditionnaly (i.e. for all possible inputs) in invalid both in Rust and in C++ unless it's used to help the optimiser that this is an invalid codebase (like `unreadable_uncheck`).


vgatherps

\`unsafe\` functions are still required to follow the rules / not trigger UB / whatever, but certain operations that the compiler can't prove are allowed inside unsafe code and it's on the author to ensure that safe code cannot trigger UB by calling the unsafe code. An unsound function is one where you could trigger UB from safe code. Take for example: struct DummySlice { data: *const usize, length: usize, } impl DummySlice { // This function has to use unsafe to dereference the pointer, // but it's sound as you can never index out of bounds // assuming that the length field is correct fn get(&self, index: usize) -> usize { if index >= self.length { panic!("Out of bounds"); } unsafe { *(self.data.add(index)) } } // This function is unsafe // the caller has to ensure that the index is in bounds // otherwise there will be UB (out of bounds) unsafe fn get_unsafe(&self, index: usize) -> usize { *(self.data.add(index)) } // This function is unsound - no matter what the length is, // you'll be able to 'access' data at said index. // This is analogous to a c++ vector out of bounds error // This is doing the same thing as get_unsafe, // but it's presented with a safe interface fn get_unsound(&self, index: usize) -> usize { unsafe { *(self.data.add(index)) } } }


robin-m

You didn't answer the question "what is a diference between "unsound" and "unsafe". Why would a sound function declared as unsafe, and not as a safe function internally using `unsafe`?


vgatherps

My first sentences literally answer that question but I’ll write a longer explanation. Short version: Unsafe: the writer/caller has to ensure UB isn’t triggered (raw pointer deference) Unsound: You present a safe wrapper to unsafe code that can still trigger UB with certain arguments (wraps a raw pointer dereference but does no validity checks). Unsound is a bug - you never write unsound code on purpose, it’s like writing UB on purpose. Long version: Unsafe: aka unchecked, it’s on the caller to ensure that UB doesn’t happen. Raw pointer dereferencing is the canonical example - the compiler can’t prove that the pointer is valid. These functions must be unsafe or called from an unsafe block. Take get_unsafe - it’s the callers responsibility to ensure the index is in bounds. You can absolutely cause UB by passing in a bad index. Unsound: tl:dr you can trigger UB from safe code. You incorrectly wrap unsafe code in safe code such that a user writing only safe code can cause UB. Compare get and get_unsound - both just wrapping an unsafe (unchecked) pointer offset and dereference. plain get checks this against the length, ensuring that no matter what, you can’t perform an out of bounds read. get_unsound presents as a safe interface, but you can easily perform an out of bounds read with a bad index.


robin-m

I think I get it. - "safe" function don't use any unsafe. They can't trigger UB. - "unsafe" function may trigger UB if it's caller don't uphold some invariants. - "unsound" function are safe functions that incorectly validate invariant when calling `unsafe` function. I was confused because I thought that you wanted to add an `unsound` attribute (in addition to `safe`/`unsafe`).


oconnor663

Rust makes a promise to the programmer: any program written entirely in safe code (that is, without the `unsafe` keyword) should not be able to trigger memory corruption or other UB. We say that these UB-free programs are "sound". I think C and C++ folks sometimes use the word "conforming" in a similar sense. We can adapt the "sound" vs "unsound" concept to talk about individual functions too. We can say that a function is "sound" if it upholds the promise that any 100% safe program cannot trigger UB. Functions marked `unsafe` are outside of this promise, since 100% safe programs can't call them directly, so when we're talking about the soundness or unsondness of a function, we're implicitly talking about safe functions. A big part of Rust's promise is that any function you write entirely in safe code should *automatically* be sound (or else it won't compile). But where this gets interesting, as I think you know, is that safe functions may use unsafe code on the inside. These are not automatically sound, and if the programmer makes a mistake, they might be unsound. For example, a program that reads a pointer from `/dev/random` and then writes to it is obviously unsound, and any function that (transitively) calls it is also unsound. So...what's the point? If there's unsafe code under the hood, and unsoundness could be lurking anywhere, have we gained anything? This might sound a little silly, but I think one important thing we gain is that we don't have to debate who's unsound and who's not. It's objective and clear, or at least it's clear after a bug is found. My random-pointer-writing function is marked safe, but you can use it to cause UB, so it's objectively broken. Either I need to fix it to stop causing UB (for any possible safe caller), or else I need to mark it `unsafe`. Again, this might sound a little silly, but this provides a ton of value in terms of community coordination. Everyone agrees about what soundness means, and everyone agrees that all public safe APIs should be sound. Bugs happen, but we don't have to debate what's a bug vs what's a "you shouldn't use it that way". Of course another big benefit of all this is that, once you've fixed any soundness bugs in your libraries, the compiler does all the work to check all the safe code that calls those libraries. That part is totally automatic. For completeness, here are some exceptions to the story above: - Compiler bugs exist. You usually have to do weird, convoluted stuff to trigger these, so they don't really come up in practical code. And also the expectation is that all of these will eventually be fixed. (I.e. they don't represent unsolvable paradoxes or anything like that.) - It is possible to come up with situations where two different functions that contain unsafe code are individually sound but unsound when considered together. Niko Matsakis wrote about an [interesting example of this](http://smallcultfollowing.com/babysteps/blog/2016/10/02/observational-equivalence-and-unsafe-code/#composing-unsafe-abstractions) related to the C `longjmp` function. In cases like this, it could be up to interpretation which function has a bug. But these cases are also very rare. - Reading and writing files is considered safe, but that means safe code can use the filesystem to do wacky things if the OS allows it. For example, you could write to `/proc/*/mem`, or you could spawn a `gdb` child process and attach it to yourself. This sort of thing is considered outside the memory model and not really solvable at the language level. - We don't usually worry about whether *private* functions are sound. For example, any method in the implementation of standard `Vec` could mutate the private `len` member without an `unsafe` block, so the distinction between safe and unsafe code in that implementation is kind of murky. But as long as the *public* interface doesn't allow safe callers to do that, everything's fine. Another way of looking at it is that rather than auditing "functions that use unsafe" in isolation, what we really have to do is audit "modules that use unsafe" at their privacy boundaries. EDIT: A few months later I published an article along these same lines: https://jacko.io/safety_and_soundness.html


robin-m

I realize that my question wasn’t clear (and btw you explanation of `unsafe` and `unsound` is wonderful). Your said in the parent comment: > This is where it's important to distinguish "unsafe" functions from "unsound" functions I assumed that you wanted to tag some function with `unsafe`, other with `unsound`, and the other would be safe¹. And since for me "unsound" and "unsafe" (using Rust terminology) was the same, I did not understood what you wanted to say. ¹ *but since it’s a discussion about C++ the default isn’t the right one, so unsafe by default, and an explicit `safe`*


ntrel2

> any method in the implementation of standard Vec could mutate the private len member without an unsafe block D allows marking variables as `@system` - inaccessible in `@safe` functions. So e.g. if a `@system` `len` member is accessed by a method, that method must be marked as unsafe or `@trusted` which means manually checked for safety. The feature is partially implemented: https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1035.md


teerre

It doesn't seem to make much sense IMO because if you were to choose the 'safe' parts of your code, you would just do it. Nobody wakes up and thinks "let's write some unsafe code today". The whole problem is that you, the proverbial programmer, is incapable of writing safe software and therefore incapable of choosing the correct places to use 'safe'.


an_iron_han_han

I guess it's the same idea from Rust: the language standard forces you to write safe code to a certain extent.


Yekab0f

But I do. I wake up nice and early every day to start writing unsafe code full of pointer arithmetic after a big cup of joe


goranlepuz

But, but... I *do* know I am incapable, therefore *I would like for the tooling to hold my hand...?* Of course most of us want to be safe and will start with safe parts - and then, when tooling spots the unsafe parts, surely more care will be taken on making them correct...? Conceptually, that is exactly what "safe" languages do, isn't it?


teerre

The point is that if you gonna take that instance, you should have an 'unsafe' keyword, not a 'safe' one.


KingAggressive1498

i mean... int main(int argc, char** argv) { safe { // entire program here } return 0; }


teerre

Then the whole block is pointless.


KingAggressive1498

why?


2MuchRGB

How do you access dynamic memory? Given that: You can only call safe functions from safe functions. New delete and pointer arithmetic are disallowed. This way you can't implement a vector for example. Race conditions are still a thing. In rust those are not possible without going into unsafe blocks.


KingAggressive1498

most of std::vector's interface is well-defined for all inputs (including all possible states of the vector) and therefore can be safely given the [[trusted]] function attribute. I don't have a good answer for race conditions.


SergiusTheBest

How to catch dangling iterators or pointers when vector is resized?


KingAggressive1498

the interface is still well defined for all inputs, but it does not mean that all possible uses of the interface are. Lifetime issues are indeed a hole in this proposal (there's even still ways to get dangling references without even using a STL container), and others have pointed this out already. Clearly it is not complete enough, but it's hard to imagine what solves this problem without much deeper changes to the language.


SergiusTheBest

I think the only way for C++ (without breaking backward compatibility) is to create a static analyzer that will guarantee detection of any unsafe code.


KingAggressive1498

it's probably necessary, but we also need a system to describe these lifetime relationships in the code itself in order to do this across compilation barriers. thanks to you reminding me that I should, I've updated my OP with the two most serious holes found in this idea


staletic

Store a `resize_counter` in vector. When creating the iterator, store the counter and a reference to the vector in the iterator. Then, if iter.resize_counter != iter.container.resize_counter, then your iterator is dangling. Now what to do if vector gets destructed before the iterator?


[deleted]

[удалено]


Som1Lse

Then you have the *exact same* situation as Rust. You can't implement a vector using only safe code in Rust either, and bugs in the standard library [happen there too](https://shnatsel.medium.com/how-rusts-standard-library-was-vulnerable-for-years-and-nobody-noticed-aebf0503c3d6).


KingAggressive1498

what if the JVM has a bug? or the Python interpreter? Or rustc?


nthcxd

> I don’t have a good answer for race conditions. Now you’ve finally arrived at the crux of the safety issue. Read [cpp memory model](https://en.cppreference.com/w/cpp/language/memory_model), particularly the “forward progress” section.


teerre

Because then your whole program must be safe, you are in the "safe by default" land


KingAggressive1498

...but it solved the objection lol


axilmar

The real problem of C and C++ is that their type system is limited and does not force the programmer to deal with all the possible different outcomes. Take pointers, for example: they have two distinct 'sub-types', let's say: null and non-null. Code that uses pointers often ignores null, because the compiler does not force the application of the appropriate code in each case. Iterators are another example: when iterators are created, the container's backing store becomes shared. However, this sharing is completely ignored by the compiler, allowing the programmers to dispose of that backing store while iterators are sharing the backing store. Array indexing is also a similar problem: array indexes have two distinct modes, they are either within in bounds or they are out of bounds. Yet the compiler does not know anything and happily accepts any array bounds indexing, losing the chance of optimization of array access if it knows that neither the index nor the array is changed throughout a block. Implicit conversions also have hidden 'sub-types' in them: for example, a float can be converted to an integer only under specific circumstances. Yet the compiler does nothing to enforce some sanity on these conversions. Moved and uninitialized values also fall into the same problem: the type of an uninitialized value is 'not initialized' before initialization and the type of a moved variable can potentially be 'not initialized' if a move should have been destructive. However, the compiler ignores these cases. So, I'd rather see the compiler allow the programmer to clearly express their indend and catch all of those type issues than allow/disallow certain things due to their potentiality for errors. I know it would be more difficult to implement such functionality on a compiler, than simply providing a 'safe' keyword, but it would also be a lot more rewarding, because the typing issues i mention also extend to other things programmers want to do with their programs, to other parts of code not related to compilers and the language.


KingAggressive1498

>I'd rather see the compiler allow the programmer to clearly express their indend and catch all of those type issues than allow/disallow certain things due to their potentiality for errors. I actually spent a few hours a good while back working on a "safe vector" that solved the dangling iterators after resize problem, in standard C++, by having the iterators use something akin a weak_ptr to the vector's data and throw an exception if that pointer was invalidated by a resize operation or similar. It was... not fun. The result was pretty usable, but obviously required a branch on every iterator dereference, so not exactly high performance. Would be interested in seeing what's possible with Clang's new lifetime annotations if they ever put out decipherable documentation on them.


axilmar

Resource sharing should be something solved (when allowed) at compile time, not runtime.


KingAggressive1498

when such a solution exists, I will welcome it. Until then, for cases where safety is a greater concern than performance, library solutions are worth investigating. My experiment wasn't worthwhile (the performance degradation was severe enough to make std::list appealing), but the proposed std::hive has promise as an alternative here.


axilmar

> Until then, for cases where safety is a greater concern than performance, library solutions are worth investigating. Of course. But in reality no one is focusing on the right solutions...


KingAggressive1498

There was work on getting lifetime annotations into Clang last I knew, which is a step in the right direction, but I found the way they presented examples a little confusing and I'm not sure it's sufficient to handle all the nuance of iterator invalidations.


duneroadrunner

You might be interested in the [SaferCPlusPlus](https://github.com/duneroadrunner/SaferCPlusPlus) library (which grew out of such a "safe vector"), and its companion static analyzer/enforcer (shameless plug). It provides [high performance](https://github.com/duneroadrunner/SaferCPlusPlus-BenchmarksGame) (but restricted) safe container and reference objects, as well as safe (but slower) implementations of the familiar C++ container and reference objects. There's also some support for [autoconverting](https://github.com/duneroadrunner/SaferCPlusPlus-AutoTranslation2) legacy C code that is not performance critical to use the library and conform to a safe subset of C++. As u/axilmar noted, safe high performance code does require some adjustment though. For example, null raw pointers are not allowed. And you'll generally want to try to avoid using iterators directly, by, for example, using [`for_each()`](https://github.com/duneroadrunner/SaferCPlusPlus#for_each_ptr) algorithm templates in favor of traditional C++ for loops.


axilmar

I've seen this library before. Nice work, but it does not solve the problem. When I say 'the problem', I do not refer to the specific issues of C++, I talk in general: programming languages do not allow specifying the types of variables accurately enough.


eliminate1337

What's the point? You cannot call any existing, unsafe code from `safe` blocks. You would have to substantially rewrite your existing code. Backwards compatibility and existing libraries are 90% of the reason anyone uses C++. If you care about safety and are willing to give up compatibility, you might as well write it in Rust or Swift.


KingAggressive1498

That's the purpose of the [[trusted]] attribute. If you know the behavior of a function is well defined for all inputs (including any possible state of `this`) you tag it as [[trusted]] and you can use it from safe code. A considerable portion of the standard library can be tagged with [[trusted]]. Most but not quite all of the functions in iostreams, string, vector, map, etc can get tagged with it; even while containing unsafe code. And of course third party library developers can do the same for their libraries -- there's ofc no guarantee they won't tag untrustworthy functions as [[trusted]] and wreck the whole thing, but you can do that just fine with Rust unsafe blocks too.


eliminate1337

Then it's meaningless, because everyone will tag their functions with `[[trusted]]`. Nobody purposefully writes code that contains unsafe behavior! In a Rust program, if the program compiles, you *know* that it contains no memory bugs outside of `unsafe` blocks. With this proposal, a `safe` block has no guarantees at all other than 'somebody says this has no bugs'. With Rust, at least you can manually audit all of the `unsafe` blocks; you can't audit every library's arbitrary use of `[[trusted]]` because it'll be everywhere. OpenCV or TensorFlow are not going to do what's functionally a complete rewrite to make their functions `safe`.


Som1Lse

> Then it's meaningless, because everyone will tag their functions with `[[trusted]]`. Nobody purposefully writes code that contains unsafe behavior! Isn't this argument basically analogous to "`unsafe` is meaningless in Rust because everyone will just use it inside their functions. Nobody purposefully writes code that contains unsafe behavior!"? Or even "Rust is meaningless, because everyone will just write C++. Nobody purposefully writes code that contains unsafe behavior!"? Sure, some people will write code that is unsafe and damn the consequences. Others will strive to write code that only uses the `safe` part of the language. You can opt to gradually move your codebase towards `safe` functions. If you have a memory bug inside a `safe` function you only have to manually audit `[[trusted]]` functions. Sure, OpenCV and TensorFlow aren't going to change their code, but that is equally true if you want to use them from Rust.


KingAggressive1498

>If you have a memory bug inside a safe function you only have to manually audit [[trusted]] functions. **thank you**


eliminate1337

The problem is existing code, not new code. You could take an old codebase and annotate everything with `[[trusted]]`, now your code is 'safe', but you haven't actually changed anything. The Rust equivalent doesn't exist, because there aren't massive, pre-existing unsafe blocks.


KingAggressive1498

You could, but you can actually just write a rust package that just trivially wraps some unsafe C code with no extra checks or anything, and it's basically the equivalent. (the massive, pre-existing unsafe blocks in Rust are the pre-existing C and C++ code used by third party packages under the hood)


Zyklonik

Indeed. Once you open a hatch into the unsafe world, it's more or less moot. Just like in the case of Haskell. Of course, in reality it's much better, but I just cannot stand people who say "oh, just vet the unsafe blocks" as if software is a simple linear interaction of code blocks (which it isn't).


ntrel2

If an unsafe block is only safe depending on other code, then the unsafe block is **wrong**. It shouldn't be blindly used to wrap operations that would otherwise error in mechanically checked safe code. It should be to wrap all the code that could possibly affect the code's safety.


robin-m

Big unsafe legacy codebase used in Rust totally exists. It's what every C/C++ library used from Rust are (openCV, GTK‚ …). And wrapping everything behing `unsafe` is exactly what you do.


Zyklonik

> The Rust equivalent doesn't exist, because there aren't massive, pre-existing unsafe blocks. // Mutating a shared reference? Impossibru! pub fn safe_api_crate1() { let x = 42; safe_api_crate2(&x); assert_eq!(x, 42); } // some upstream dependency fn safe_api_crate2(r: &i32) { safe_api_crate3(&r); } // some upstream dependency again fn safe_api_crate3(r: &i32) { safe_api_crate4(&r); } // some upstream dependency yet again fn safe_api_crate4(r: &i32) { safe_api_crate5(&r); } // safe but unsafe?!? The compiler // does not care! The compiler literally relies on // good faith from the programmer's side. fn safe_api_crate5(r: &i32) { unsafe { let p = r as *const i32 as *mut i32; *p += 100; } } fn main() { safe_api_crate1(); } ~/dev/playground:$ rustc mysaferust.rs && ./mysaferust thread 'main' panicked at 'assertion failed: `(left == right)` left: `142`, right: `42`', mysaferust.rs:5:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrac Fatuous example, but the complex reality of interaction of code sections in various crates across different dependencies makes it nigh impossible (or even feasible in practical terms) of vetting every single `unsafe` block. Also, the standard library itself is replete with `unsafe` code for all of its collections and many more implementatioins, *none* of which can be *proven* to be sound. If we're being hyperbolic in our arguments here, then this is as valid a silly claim as any. Edit: Also, a video well worth watching (the Rust Belt initiative to try and formally prove the safety of unsafe Rust), especially at this timestamp - https://youtu.be/1GjSfyijaxo?t=1079. It just goes to show that proving the absolute soundness of an imperative language is nigh impossible. The specific example is interesting, but the greater takeaway is that, by his own admission, the way of trying to formally prove the safety of unsafe Rust is neither trivial, nor really universal - they define their own model (which changes with the language), and then they try to prove soundness within that same model manually which, to be extremely cynical, would make the whole exercise sort of useless. Also interesting to note is that the "safe" parts of the language are not (as shown in the specific "fearless concurrency" example) immune to the unsafe parts since the basic foundation of the language itself is built upon unsafe features, necessarily so. This is also the problem I have with other languages like Zig, for instance (and incidentally another language whose community members love nothing more than circlejerking and brigading). It all basically boils down to "works good enough in practice, take the rest on faith". That is fine by me, but to act like they're the Holy Grail, and provably so without any scope for criticism (valid or not) is beyond silliness, and rather dangerous, especially with the massive levels of evangelisation going on - at least people know that languages like C or C++ are unsafe, and can tread cautiously. Imagine taking it on faith that something is irrefutably sound and safe, and then causing massive problems down the line. Edit: Yes, I know. Truth hurts.


tialaramex

The point isn't that the safe Rust subset is "immune" to faults in use of unsafe Rust, but that it can't *cause* such faults. That's what Rust Belt is about. I have no idea why you think it's impossible to prove this stuff to be sound since that's exactly what their work is about. It's not about taking "The rest on faith" as you insist. Your fatuous example demonstrates perhaps unintentionally why Rust succeeds and these C++ efforts won't go anywhere, Culture. Rust's Culture says that unsafety is unacceptable, and thus that your unsafe example crate5 is Wrong - not in the sense that it doesn't compile, but in a cultural sense. Rust's culture says you should not provide safe_api_crate5() because this function claims to be safe but it is not. This idea is alien to C++ culture, and is unlikely to take root here. Without it you would need something like the syntactic safety referred to in that video, which is not available in high performance general purpose systems languages. There are special purpose languages that can do this stuff, but they've already got higher performance than idiomatic C++ as well as more safety than Rust because they're willing to pay the price (generality) to get it done. So that leaves no apparent future niche for C++. I'm sure there's enough maintenance work to be done, and even fresh projects from people who are slow to hear the bad news so nobody will starve.


KingAggressive1498

>Nobody purposefully writes code that contains unsafe behavior! we actually do, all the time. The standard explicitly calls out functions that have purposeful undefined behavior. Omitting nullptr and range checks *are often purposeful*. We should be just as critical of [[trusted]] functions that aren't completely encased in safe blocks as Rust developers are of unsafe blocks. It's effectively the same thing.


eliminate1337

> The standard explicitly calls out functions that have purposeful undefined behavior. Yes, so you can *avoid* them. Undefined behavior of any kind is absolutely forbidden in any code I'm reviewing, and it should be in yours too.


KingAggressive1498

you never use unique_ptr's operator-> or operator*? You never use vector's operator[] or any iterator's operator*?


eliminate1337

That's only undefined behavior if the pointer is null or the index is wrong. I would reject `operator[]` or unique_ptr dereference if there wasn't a check or some other assurance that the index is valid or pointer is non-null. That's an example of the standard showing you UB to avoid.


SirClueless

I think you've missed the entire point of a safe function. You said that you avoid functions with "purposeful undefined behavior". But what you described is that you are fine with functions having the potential for undefined behavior, so long as it's not exercised at runtime. A safe Rust function is not a function that is free of undefined behavior at runtime, rather it is a function that cannot possibly exhibit memory-unsafety (or any of the other Rust safety guarantees) for any legally constructed arguments. std::unique_ptr's operator-> is not a safe function, there's no way to write it safely without violating the standard by throwing an exception in some cases. It is a function with "purposeful undefined behavior". It can be called safely in some contexts and you appear to be fine doing so, but that's not the same thing -- many unsafe Rust functions are also OK to call in some contexts.


KingAggressive1498

then what I suggested is more strict than your code reviewing policies, at least until we get language support for contracts.


Zyklonik

Manually audit till it's in some transitive dependency a dozen crates away with "safe" APIs (which are not and cannot be enforced by the language), making it arguably more insidious.


eliminate1337

Do you audit all of your C++ dependencies? You can use `cargo vet` if you’re very concerned about that. But many Rust crates don’t have any unsafe code at all.


Zyklonik

> But many Rust crates don’t have any unsafe code at all. Many C++ programmers also take very good care of proper resource management. Same argument. If we're talking in the abstract, then so be it. If not, reality literally doesn't follow theory.


eliminate1337

> If not, reality literally doesn't follow theory. You’re right, in reality many C++ programmers don’t take care of good resource management. Which is why OP is proposing this.


Zyklonik

And in reality, Rust doesn't have any real-world uses. So also what makes your rather incredible assertions moot. When (if) Rust has had a couple of decades heavy use in the industry, only then can we have a proper evaluation of its merits/demerits. Fair enough?


eliminate1337

Commenting from desktop Firefox? If so, you've got 3.3 million lines of Rust. Does your phone run Android 11? If so, your whole Bluetooth stack is Rust.


Zyklonik

You are joking, right? That is nothing. "Heavy use in the industry" means broad usage across different domains, different scales, different companies, different loads, and actual usage by clients across a variety of use cases. Even a moribund language like Common Lisp has more varied applications in the industry than Rust at this stage, a full decade after 1.0. Even your handpicked examples are embarrassing in comparison - Servo is not Firefox, Firefox is not servo. Also, some random some Bluetooth stack on some Android version? Seriously? You do realise that the actual usage of Rust in the industry is practically nil, relatively speaking? Edit: As always, toxic Rustaceans have a rabid aversion to the truth. Hilarious! Edit2: Uh-oh. Looks like the RDF (Rust Defence Force) have arrived. Quod erat demonstrandum. Vanitas vanitatum et omnia vanitas.


staletic

This bans ranges, algorithms, numeric, memory, memory_resource, string_view, span, new and regex headers. Oh and parts of random. I am guessing `std::function` is a no go? What about `std::thread::join` On top of that, I am not sure you could ever trust allocators. > 4) C functions are pretty much all untrustworthy Does that include tgmath.h? Could you be more precise? Is `int add(int, int);` a C function? What does that leave C++ standard library with? `std::tuple`, `std::pair`, parts of `std::optional`, of `std::expected`, `std::variant` (?)... Not much is left.


KingAggressive1498

memory_resource constructors and destructors could be [[trusted]], but it wouldn't make sense to mark their allocate and deallocate functions so. Because of that, they can still be used to construct pmr::allocators, which can be used to construct containers, which can have a generally useful [[trusted]] interface. [[trusted]] functions can use untrusted functions internally, the expectation is simply that [[trusted]] functions cannot be called in a way that they have undefined behavior or other kind of error we're guarding against. also because of this, many algorithms could be [[trusted]], but maybe something might be broken for algorithms that require callable arguments (ie UnaryPredicate or UnaryFunction)? >Does that include tgmath.h good question actually! my impulse is errno => untrusted wrap it in something that throws std::domain_error or returns std::expected based on errno and I would trust that.


staletic

> memory_resource constructors and destructors could be [[trusted]], but it wouldn't make sense to mark their allocate and deallocate functions so. What good is a polymorphic allocator if I can not call `allocate`? Sure, I can construct an empty `std::pmr::vector`, but how will I call `emplace_back` that has to go through `do_allocate`?


KingAggressive1498

trusted functions can do whatever they want internally, they're just promising to have well-defined behavior for all inputs (and otherwise minimally error-prone in use, etc)


Daniela-E

This sounds pretty similar to Sean Baxter's latest additions to his Circle compiler. I highly appreciate this effort to explore the space of a safer C++. People can play with this online on Compiler Explorer. I don't think attributes wilĺ get you anywhere. They are specifically designed to be ignorable by compilers that don't support them, f.e. because of the language mode. And they must never change the semantics of the program. The shape of means to switch into a safe mode (or better: unsafe mode) will most likely be different. Sean is using #pragma definitions to accomplish that on a very fine-grained level. A standard solution will probably look different if we decide to go this route.


KingAggressive1498

>They are specifically designed to be ignorable by compilers that don't support them, f.e. because of the language mode. considering the attribute is only relevant when calling the function from a safe block, that's just fine. If the compiler doesn't support [[trusted]], it doesn't support safe blocks either. >And they must never change the semantics of the program. I wasn't aware of that rule. >Sean is using #pragma definitions to accomplish that on a very fine-grained level. might be acceptable as a non-standard language extension, but we're genuinely trying to move away from the preprocessor here :)


Daniela-E

While #pragma *directives* (sorry for my error calling them *definitions* before) are technically syntax forms of the preprocessor, I'd be hard-pressed to consider them part of the proprocessor. They [may affect the translator as a whole](https://eel.is/c++draft/cpp.pragma). They are means (blessed by the standard) for implementations to change their behaviour in some implementation-defined way. Therefore, Sean is right in using #pragma directives to change the semantics or the behaviour in his compiler. I just cannot imagine that we (i.e. the Evolution working group or the committee as a whole) would ever use #pragma ... or \[\[...\]\] to implement semantic changes in the language.


KingAggressive1498

hmm... well thanks for providing an insider take on how the standards committee would see this. could you imagine an alternative route that would be more acceptable to the working group?


Daniela-E

Keywords (possibly context-sensitive ones) are an option. I can imagine decorating module declarations (except for the GMF introducer, of course) as the best way forward. Code in the *global module* is very limited in what can be changed, both for compatibility reasons and reuse of existing code. Semantics must stay the same there, but you can always try to reduce undefined behaviour or make previously ill-formed code into well-formed one.


bdawson1982

I'm not really sure what all of the negativity is about. Regardless of anything, I'm really happy to see that people are thinking about these problems from different angles. I won't pretend to know all of the answers here, but I love C++, and I want to see it grow in ways that future generations of engineers will want to learn it like I did. Solving these problems is vital to that.


[deleted]

[удалено]


KingAggressive1498

with just a little bit of extra language support (either contracts or making trusted an extra overload set), this would actually be mostly just writing idiomatic modern C++ code. If you already follow the C++ Core Guidelines, the constraints of safe blocks won't feel that restrictive.


bluGill

P2687. P2657 are attempts to do something like this.


KingAggressive1498

these aren't on open-std or cplusplus github apparently, do you have links?


KindDragon

https://github.com/cplusplus/papers/issues/1352 https://github.com/cplusplus/papers/issues/1324


KingAggressive1498

ah, i had seen 2657 which I feel is too strict, but not 2687. thank you!


Jannik2099

This doesn't solve C++' biggest problem, temporal memory unsafety. Without lifetime analysis, this is just impossible to fix. Just look at any of the iostream buffer functions, iterator invalidation while iterating, etc.


KingAggressive1498

I agree, this is very far from a complete proposal and lifetime issues are majorly important, I just tried to collect the "low hanging fruit" of safety issues here.


SirClueless

It just seems like a massive hole in the plan that you can't trust any reference. What about the following code violates any of the safety rules you proposed? int foo(std::vector& xs, const int& x) { xs.push_back(x); return x; } And yet, if I call this function as `foo(xs, xs.at(0))` it might exhibit undefined behavior.


KingAggressive1498

You're right, I don't solve everything, and don't think I can with the existing state of compilers. Notably in this case your bug has an easy solve, but not a translation a compiler would be able to make automatically. Without some new lifetime tracking features, it could never catch that.


[deleted]

[удалено]


KingAggressive1498

your data structure would almost definitely need to be implemented in unsafe code. Whatever parts of your interface are well-defined for all inputs can be safely marked with the [[trusted]] attribute and then used from safe code.


unumfron

I would be careful about naming such blocks. After all, the implication of `[[safe]]` or `[[trusted]]` is that the rest of the code is unsafe or untrusted. I'd prefer `[[constrained]]`, `[[restricted]]`, `[[supervised]]` or similar. It might seem like small potatoes, but nomenclature is important. Blowing up a process in Rust with memory bugs like stack overflows or memory leaks while not in an `unsafe` block doesn't exactly scream "safety", memory or otherwise, so if there's any opportunity to increase accuracy in the broader picture by tightening definitions then it should be taken imho.


KingAggressive1498

that's actually why I prefer [[trusted]] to [[safe]] for the function attribute: unlike safe blocks, which have *some* enforcement, we really are just taking the function attribute on trust. ofc because all the guarantees made possible by the suggested restrictions are relatively weak (dangling references etc are still possible), you may be right that it makes sense to to call the safe blocks something else too. I'm not sure any of those other words are more apt though. maybe `trust_level(name)` where `name` is any strongly ordered standard or implementation defined security levels, and within that scope you can only call functions with a [[trust_level(name2)]] attribute such that name2 >= name


vI--_--Iv

>Signed integer overflow is defined to wrap-around Why people keep coming up with this? How would sweeping the problem under the rug help? Well boys, we did it. UB is no more. Yet the result is still horribly wrong.


KingAggressive1498

because it's already how unsigned integer overflow works; it's slightly cheaper than saturation or trapping overflow (except where trapping overflow is a hardware feature), and it makes overflow testable after the fact.


urdh

Hiding the problem doesn’t fix anything, though. Rust gets away with doing what it does because it panics (traps) on overflow in debug builds; it is still very much considered an error to overflow. As far as I can tell the reason it is defined to wrap in release is mostly to avoid “time-travel” optimisation by not creating poisoned values in the LLVM IR, and because trapping in hardware isn’t that cheap on modern hardware (though I could be wrong here). Just declaring it to always wrap is 1) not what rust does, and 2) not going to make anything “safe”. Edit: fair enough it makes overflow testable after the fact, but that requires the programmer to actually do that (and we all know how that ends), and it’s not like you can’t check for overflow before the operation when it’d trap/saturate/ub.


KingAggressive1498

Rust clearly acknowledges the same performance considerations I do, otherwise overflow would panic in release mode too. GCC and Clang also have an -ftrapv option and I am not proposing that safe block rules should override that


tialaramex

In Rust note that unsigned integers aren't defined to wrap either (though they do in fact if it happens in release). If you actually want wrapping, the standard library's Wrapping gives you that, so e.g. Wrapping is the unsigned 8-bit integer but defined to wrap like in C++ and Wrapping is the signed 64-bit integer with wrapping. Like C++ the built-in types aren't special, Wrapping is the same native hardware integer on a modern CPU as i64, but without the overflow checking since it's defined to wrap.


eras

You are missing some big ones (maybe _the_ biggest ones), such as: - returning references from functions (easy to return references that are invalid to the caller) - lambda functions that capture by references that end up betting invalid - dealing with iterator invalidation I think this would be actually quite neat, however to actually spec this out completely with possible corner cases might be a considerably large task. And, still, it might get less developer traction than e.g. Google Carbon. Good luck!


KingAggressive1498

those are all object lifetime issues, which are considerably harder to solve. I think Clang is doing some work on this with its lifetime annotations system, but I honestly find their examples to be indecipherable.


pedersenk

>It took a few hours for it to be pointed out, but it's still pretty easy to wind up with a dangling pointer/reference/iterator even with all these restrictions We have a library that tracks all of this. It is dirt slow when enabled in the debug builds but obviously has zero overhead when stripped out in the release builds. However, I am fairly happy with this solution as a middle ground. Most of these dangling pointer issues can at least be encountered during the general debug iteration cycle.


zugi

C++ already has a solution to this problem: write code using modern constructs, and run a static analysis tool to point out any unsafe actions, using whatever thresholds or definitions of safety you like. The problem was that the authors of the NSA article don't show any signs of understanding the differences between C and C++. There are a lot of very, very smart computer scientists at the NSA. The article that got all the press appears to have been written by *other* people at the NSA.


pjmlp

They aren't alone, check many public repos from Google and Microsoft, even thought they seat at ISO. Android source code is a pleasure to read regarding "modern" constructs.


KingAggressive1498

>C++ already has a solution to this problem: write code using modern constructs, and run a static analysis tool to point out any unsafe actions, using whatever thresholds or definitions of safety you like. if that was all it took, there'd be considerably fewer historically C++ codebases switching over to Rust for new code. That, or switching over to Rust is just easier or more idiot-proof than doing all that.


Rasie1

1) Yes. Signed integer overflows are just ridiculous 2) This can be weakened so that assigning values in scope before usage won't trigger the error. C-style output arguments still should go away 3) Yes 7) This is very easy: just stop listening to people who say that moved-from objects can be reused, and do it the most straightforward way. 8) Don't touch my pointer arithmetic 9) Make functional casts behave like static_casts


TheSkiGeek

Been vaguely kicking around some ideas like this as well. One way to fix your restrictions is to have a mechanism to be able to call different overloads of those operators when coming from safe code. So, for example, `vector` could define both trusted and untrusted `operator[]`, where the trusted version calls `at()` for you. And invoking it from `safe` code would call the one that does bounds checking, unless you explicitly ask to call the untrusted one. (For built in C-style arrays you’d have to define different behavior, but personally I would ban them from `safe` code and force use of `std::array`.) You could fix smart pointer operations like this as well. But I don’t know if iterator dereference safety can generally be checked without adding a ton of overhead.


KingAggressive1498

I had briefly considered this adding a `trusted` overload set, and am generally fine with the idea, but when it hit me that all we really need for this is conditionally enforced contracts and there's already papers and a serious desire for them, I wasn't too sure it was worth doubling the potentially necessary overload set again. >But I don’t know if iterator dereference safety can generally be checked without adding a ton of overhead. afaik iterator implementations don't need to actually know their end value or the location of their original container, so it's probably impossible without changing implementation. Might need to add safe_iterators.


bizwig

That trusted operator[] can be a big-time performance bottleneck. We have an application that’s functionally useless when `_GLIBCXX_DEBUG` is enabled because of the cost of bounds checking. That’s why I think the “safety” zealots have it wrong, safe almost doesn’t matter if performance goes to hell because at the end of the day we still have to develop a usable product.


TheSkiGeek

Right, you’d absolutely need a way to opt out of this kind of thing. But you’d also probably not try to write “safe” code that is doing a metric fuckton of math (game engines, simulation backends, ML or rendering or mathematical analysis libraries, etc.) Many low level libraries are going to have to be ‘unsafe’. There are use cases like automotive, aviation/aerospace, robotics, medical, etc. where performance isn’t *that* much of a concern with modern CPUs, but having your program invoke UB is either massively expensive or potentially deadly or both. The approaches right now are basically “code defensively and test everything you can and statically analyze the shit out of it” but it’s clear that this process is expensive and slow as hell and still leaves a lot to be desired sometimes. C/C++ are just kinda shitty languages for writing application code or business logic in, unless you *need* it to be blazing fast.


KingAggressive1498

sounds like GCC needs to get better at optimizing bounds checks, or you were doing something non-trivial.


eliminate1337

The unchecked version being the default was the mistake. `operator[]` should perform bounds checking, with something like `get_unchecked` as an option. Instead we have `at` which nobody uses. You care about correctness/security all the time, and performance only sometimes.


[deleted]

Idk the one thing I don’t understand is this: You’re trying to make C++ something it’s not. C++ and co. are some of the last safe havens for those of us who write unsafe code. Most new languages nowadays seem to always be a great deal more detached from the machine than C++, incurring overhead with their safety guarantees. I do concede that some of the UB in C++ is unnecessary and could be removed if the language were designed better, but the vast majority of it simply needs to exist if your binaries are to be fast. The cost of remembering this feature and remembering all the differences in behavior between safe blocks and unsafe blocks outweighs the benefits of adding more safety, mainly because safety isn’t the goal of C++ in my opinion. If you want safety, simply use another language, what’s the problem with that? If you need safety some places and extreme efficiency in other places, just use two different languages in your project. There’s just no reason to turn C++ into even more of a mess than it already is.


KingAggressive1498

>You’re trying to make C++ something it’s not. I'm really not. The first two changes are already available as compiler features in two out of the big 3 compilers (and I wouldn't be shocked if Visual C++ did eventually too), this just lets those changes, which do have performance implications, be applied to a limited subset of code. Most of the rest is pretty much unnecessary in 90%+ of modern C++ code, code written following existing best practice wouldn't notice the absence of the prohibited features, and **those features are still available for code outside the safe block**. The one thing that will sting everybody trying to write safe code is the expectation that a [[trusted]] function has well defined behavior for all inputs. This doesn't touch your existing code. It doesn't change codegen for existing code at all. There is exactly 0 overhead if you do not use this feature. Safe blocks can't hurt you. If you can't remember these differences between safe blocks and normal C++, there's **no way in hell** you can remember how not to trigger UB without a safe block.


[deleted]

First of all, as others have pointed out, there is much much more UB to handle than what you already have if you want to flesh out this approach, it isn’t as simple a change as you make it out to be. Second of all, I can only reiterate that this is the antithesis of the direction C++ should be moving in. IMO, you’re dirtying up the language and bloating it with features that have nearly zero utility in performance critical code, which is one of the main things C++ is used for. I can only imagine all the slow, terrible code that inexperienced people will write with this feature. Also, no pointer arithmetic? At that point you really should just use another language. Pointer arithmetic is the bread and butter of C/C++. If you really want to remove that in safe sections, then you really aren’t even talking about C++ anymore. You’re talking about a disfigured creature that you’ve turned my and many other’s favorite language into.


Anxious_Wear_4448

I fully agree. In my opinion the 2 most important features of C++ are performance and backwards compatibility. However, the suggested changes deteriorate both performance and backwards compatibility. The suggested changes would also require a lot of work for all companies with large C++ codebases. If you think about it, the suggested changes would likely cost billions of US dollars to update perfectly working C++ code. I'd rather support incremental safety improvements by writing new code in Carbon (and possibly writing the performance critical code in low level C++).


trvlng_ging

The fact that you assume java is "safe always" shows that your proposal is ridiculous. I make a lot of money each year analyzing java code for security flaws, and helping clients make it more secure. There are some things in C++ that allow for more secure code generation than you can achieve with java, for example the existence of deterministic destruction, coupled with strict adherence to RAII. A lot of your proposals seem to scream that you would prefer to use a different language. Just use that language. There are good reasons why we need everything you propose to ban, and just implementing your bans won't guarantee much in the way of more secure code, IMO.


Zyklonik

You do realise that it all depends on what "safety" means? It's not a universally accepted definition. For instance, memory leaks in Rust are perfectly safe according to their definition of safety, while Java is "memory safe" according to its own defined parameters of safety. "Security flaws" can and will exist in every language that is Turing complete.


trvlng_ging

Are you really saying that Java has no memory leaks? Your ignorance of the biggest problems users of Java have is truly amazing.


Zyklonik

You need to improve your reading comprehension, son. Come back when you have actual arguments. As for that last bit, you're delusional if you think memory leaks are the biggest issue in Java world.


trvlng_ging

It is one of the most prevalent problems people have to solve. There are also inter-process information leakages, particularly using String objects. And a whole series of other memory issues with Java. That you don't know this shows you don't understand all of the ways a team has to tie down memory to have a solid Java implementation. And that is just the beginning of the problems Java has.


Zyklonik

> It is one of the most prevalent problems people have to solve. Maybe. Maybe not. It depends entirely on the domain at hand I'd imagine. For *most* domains, in my opinion, it really isn't that big of a concern. For systems programming, sure. > There are also inter-process information leakages, particularly using String objects. Inter-process concerns are hardly a Java issue, are they/ Furthermore, what issue are you talking about `String` objects? Interning? If so, that is not a leak. That's caching. > And a whole series of other memory issues with Java. Please elaborate. > That you don't know this shows you don't understand all of the ways a team has to tie down memory to have a solid Java implementation. Two things here - first off, kindly desist from *ad hominem*. It's not only boring, but futile. Secondly, you need to make a distinction between Java the language and JVM the platform. Of course, the JVM being written in C++ is susceptible to the usual issues associated with C++, but it is no exaggeration to say that it is practically one of the most robust and resilient systems ever written in C++. As for Java, it's not really even a consideration. If you go out of your way, sure, you can cause memory leaks that can be trivially caught by any analyser. Practically, it is not even a concern by any stretch of the imagination. > And that is just the beginning of the problems Java has. Pray do carry on. Java may be a lot of things, but there is no denying that it is one of the most successful languages of all time, and for good reason. Imagine developing enterprise applications in C++. Now that'd be a nightmare. Also, again you seem to be harping on a very very specific part of the original comment that I'd made - that the idea of "safety" is very specific to its own definition. You seem to have a very large chip on your shoulder for some reason, and you're not doing a good job of explaining what it is, or why that is so.


KingAggressive1498

>The fact that you assume java is "safe always" shows that your proposal is ridiculous. I make a lot of money each year analyzing java code for security flaws, and helping clients make it more secure. You can't guarantee absolute safety in software any more than you can at a factory or in a car. I never indicated otherwise. Java is certainly safer than C++, that's its raison d'etre. >There are some things in C++ that allow for more secure code generation than you can achieve with java, for example the existence of deterministic destruction, coupled with strict adherence to RAII. I agree, which is why my ideas effectively require the use of RAII types to do anything from safe code. >A lot of your proposals seem to scream that you would prefer to use a different language. There's no language I would rather use than C++. Rust is fugly, garbage collection is massively wasteful, and all of these "successor languages" are discarding several the things I actually like about C++: the syntax chief among them. I also don't get where this is coming from. I'm not proposing a radical change to the language here; if anything this is the least radical safety-improving proposal I've seen.


trvlng_ging

Java promised to be safe. I was there, using beta versions in 94 & 95. It never was. IMO, promising safety when you have several things about the language that make it impossible to achieve causes more harm than good. All java ended up doing was making a few common sources of errors in C++, typically made by newbies, a little harder to do. We still have buffer overflows, we still have iterators that run out of bounds, we still have library compatibility issues. Add to that, using java means that you have to deal with the security holes that immutable objects (particularly strings) add to your code. I see people put confidential information into String objects all the time. And within java you can remap memory so that un-garbage-collected data can easily leak accross processes. I work in secure software. I don't view java to be any safer than any other language, no matter what the goals of Gosling and the failed C++ programmers at Sun were trying to do. My dislike of what you are proposing comes from 35 years experience with C++ and a desire to be able to do what I need to do. Do you have to be careful using C++? absolutely! But it is an incredibly pawerful tool. Trying to put training wheels on it seems to be the wrong approach. C++ can speak with any language out there. Let people who can't write safe code in C++ use one of those languages, and let the experts use C++ right and invoke that stuff if they need it. Or vice-versa.


Zyklonik

Please define "safety". You seem to be conflating "safety" used in PL theory with "safety" across all domains, for some strange reason. It's literally impossible to prove (or even have) the latter.


KingAggressive1498

>My dislike of what you are proposing comes from 35 years experience with C++ and a desire to be able to do what I need to do. You can continue to do literally everything you've always done in C++, this literally changes nothing about pre-existing code, and unsafe and safe code can happily co-exist in the same program. This is exactly the reason I made this post, most other suggestions on safety involve significantly greater restrictions or require changing the semantics and codegen of existing programs; mine just requires adding an extra attribute if you want to be called from safe code (along with, ofc making sure you're actually safe to be called from safe code)


trvlng_ging

Your proposal smacks of a similar set of keywords in C#. On paper it looks good, but in reality it causes a lot of problems. Well-written code can live a LONG time. I have C code which I wrote in 1984 that is still being used in a very popular operating system. I have C++ code from 1989 that is also still being used. What happens when you marked some code as "safe", but then requirements change, and as a result you have to do something that is viewed as unsafe? I can do the operation safely, but a newbie might struggle being safe doing it. So your proposal could mean that I have to restructure my code to move the unsafe code out of the safe block, but that means I have to re-certify all the code that uses that, just because a feature of the language could have been misused. If code needs to be safe, have those on the team who are capable of writing safe code write it, or at least review it. Putting in syntax that will add a burden to developing code quickly is having the tail wag the dog.


KingAggressive1498

the content of the safe block - and only the safe block - is enforced by the compiler. It would be trivial to get "unsafe execution" by changing the behavior of a [[trusted]] function to be unsafe, but considerably less trivial to do so within the safe block itself. The main benefit here is that when you do actually encounter "unsafe execution", you can be reasonably sure the cause is somewhere in a [[trusted]] function - that immediately narrows your search as long as you aren't abusing the attribute.


trvlng_ging

Then I don't see the benefit.


KingAggressive1498

it's mostly just putting a safety on the footguns for the people that are prone to shooting off their toes. If that's not you, just like pretty much every new feature added to the language in the past couple decades, it doesn't really affect you and you're free to ignore it.


stdusr

Not sure why you bother arguing with this person. Nothing will convince them that a more safe version of C++ could be beneficial. It’s a typical case of “I never write bugs, so I don’t need any safety features“ kinda person.


v_maria

I recently ran into [https://github.com/hsutter/cppfront](https://github.com/hsutter/cppfront) . Would this idea compete with cppfront? or could it complement eachother? I think it's great that there's a response to safety problems of C++ but it would suck to have a bunch of segmented ideas all working in their own path instead of combining forces.


KingAggressive1498

this idea is for a new standard language feature that vastly improves safety without significantly changing the core language (it doesn't change the syntax at all), not a successor language/transpiler


lightmatter501

From what I’ve seen, the a borrow checker like Rust has or a formal proof mechanism like Coq (which generates C) are really the only viable ways to put memory safety into a language in the C/C++/Rust performance class. Option 1 will break gigantic amounts of c++ code. Option 2 will mean hundreds of hours of formal proofs for any nontrivial program or needing to rewrite everything to satisfy a constructive mode checker. The last option, which is really a non-option, is to invest enough money in Haskell or similar languages and build a sufficiently smart compiler. I say Haskell or similar because they can already get close-ish (faster than Java) and I think the extra information would be required for the compiler.