The problem I have with C++ (and I've written a lot), is every programmer involved in your project has to be 100% perfect, 100% of the time.
I know you can run memory checkers, and STL debug modes, but unless you are running these in release as well (is anyone doing that?), then you are then counting on your testsuite hitting every weird thing a silly, or malicious, user might do.
Given how important software is nowadays, and how Rust seems close to the performance of C++, is it really worth using a language where so many of the basic fundamental features are incredibly hard to use safely? They keep making it worse, calling an empty std::function threw an assert, the new std::copyable_function has made this undefined behaviour instead!
Are you talking about dereferencing a null pointer? That is going to case a low level exception in any language, it has nothing to do with std::optional.
every time you write v[x]=y you aren't inviting the possibility of arbitary memory corruption
That's how computers work. You either pay the penalty of turning every memory check into a conditional boundary check first or you avoid it all together and just iterate through data structures, which is not difficult to do.
Also debug mode lets you turn on the bounds checking to debug and be fast in release mode.
The reality is that with a tiny amount of experience this is almost never a problem. Things like this are mostly relegated to writing your own data structures.
It was originally discussed as a way of avoid null pointers, for the common case where they are representing the possibility of having a value. However, it has exactly the same UB problems as a null pointer, so isn't really any safer.
I'm going to be honest, saying that memory corruption in C++ is 'almost never a problem' just doesn't match with my experience, of working on many large codebases, games, and seeing remote security holes caused by buffer overflows and memory corruption occurring in every OS and large software program ever written.
Also, that 'penalty' really does seem to be tiny, as far as I can tell. Rust pays it (you can use unsafe to disable it, but most programs use unsafe very sparingly), and as far as I've seen benchmarks, the introduced overhead is tiny, a couple of percent at most.
I don't think this is true. I don't think it has anything to do with null pointers, it is a standard way to return a type that might not be constructed/valid.
I think you might be confusing the fact that you can convert std::optional to a bool and do if(opt){ opt.value(); }
You might also be confusing it for the technique of passing an address into a function that takes a pointer as an argument, where the function can check the pointer for being null before it puts a value into the pointer's address.
I'm going to be honest, saying that memory corruption in C++ is 'almost never a problem' just doesn't match with my experience,
In modern C++ it is easy to avoid putting yourself in situations where you are calculating arbitrary indices outside of data structures. Whether people do this is another story. It would (or should) crash your program in any other language anyway.
Also, that 'penalty' really does seem to be tiny
The penalty is not tiny unless it is elided all together, which would happen in the same iteration scenarios that in C++ wouldn't be doing raw indexes anyway.
The paper that proposed std::optional literally uses examples involving null pointers as one of the use cases that std::optional is intended to replace:
https://isocpp.org/files/papers/N3672.html
Here is an updated paper which is intended to fix some flaws with std::optional and literally mentions additional use cases of nullptr that the original proposal did not address but the extended proposal does:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p29...
I am going to be somewhat blunt, but if you're not familiar with how some of the use cases for std::optional is as a replacement for using raw pointers, along with some of your comments about how C++ treats undefined behavior, as if it's just results in an exception, suggests you may not have a rigorous enough understanding of the language to speak so assertively about the topic.
I never said there wasn't a use case, I said it wasn't specifically about protecting you from them. If you put a null pointer in and reference the value directly, it doesn't save you.
If you don't understand the context of the thread start arguments over other people's simplified examples. I'm not going to write huge paragraphs to try to avoid someone's off topic criticisms, I'm just telling someone their problems can be avoided.
"I don't think this is true. I don't think it has anything to do with null pointers, it is a standard way to return a type that might not be constructed/valid."
This was in response to:
"It was originally discussed as a way of avoid null pointers,"
I have provided you with the actual papers that proposed std::optional<T> and they clearly specify that a use case is to eliminate the use of null pointers as sentinel values.
I'm not sure what your point here is other than to try to mince words and argue. It's a standard way to put two values together and what people were probably already doing with structs. You can put pointers into a vector too, but that doesn't mean it's all about pointers.
> The behavior is undefined if this does not contain a value.
The equivalent to this in Rust isn't `Option::unwrap`, which will (safely) panic if the value isn't present; the equivalent is `Option::unwrap_unchecked`, which can't be invoked without manually marking the code as unsafe. I've been writing Rust professionally for a bit over five years and personally for almost ten, and I can definitively say that I've never used that method a single time. I can't say with any amount of certainty whether I've accidentally used the deference operator on an optional type in C++ despite writing at least a couple orders of magnitude less C++ code because it's not something that's going to stick out; the deference operator gets used quite often and wouldn't necessarily be noticeable on a variable that wasn't declared nearby, and the compiler isn't going to complain because it's considered entirely valid to do that.
[1]: From https://en.cppreference.com/w/cpp/utility/optional/operator
That's just fundamental difference of opinion, Rust isn't designed for efficiency, it's designed for safety first. C++ unofficial motto is, don't pay for what you don't use.
If I type *X why would I pay for a check if its empty, I literally should have checked the value isn't empty.
If you work in a code base with people who don't check, your codebase doesn't have static analysers, you do no code review and dereferencing an uncheck optional get's to production, do you think a .unwrap in rust wouldn't have made it to production?
Also, an unwrap isn’t perfect, but it’s much better than UB. It asserts. No memory corruption, no leaking all your user’s data, no massive fines.
The equivalent to C++ would be an unchecked unwrap in an unsafe code block, and that would throw up flags during review in any Rust codebase.
EDIT: an unwrap that crashes in a panic is a dos condition. In severity this might be worse or better depending where it happens.
Both are programmer error, both should be caught in review, both aren't checked by the compiler.
Citation needed, because Graydon Hoare the original Rust creator (who has not been involved with Rust development for quite a long time) wrote about how the Rust that exists is not like the original one he was designing:
- "Tail calls [..] I got argued into not having them because the project in general got argued into the position of "compete to win with C++ on performance" and so I wound up writing a sad post rejecting them which is one of the saddest things ever written on the subject. It remains true with Rust's priorities today"
- "Performance: A lot of people in the Rust community think "zero cost abstraction" is a core promise of the language. I would never have pitched this and still, personally, don't think it's good. It's a C++ idea and one that I think unnecessarily constrains the design space. I think most abstractions come with costs and tradeoffs, and I would have traded lots and lots of small constant performancee costs for simpler or more robust versions of many abstractions. The resulting language would have been slower. It would have stayed in the "compiled PLs with decent memory access patterns" niche of the PL shootout, but probably be at best somewhere in the band of the results holding Ada and Pascal."
I think you would have a hard time convincing the C++ standards committee to put a checked container in the standard, maybe now with the negative publicity maybe but definitely not before.
I'm guessing it would be impossible to get an unchecked container into the rust stdlib.
There clearly are people who think that UB isn't as dangerous as I do, so if that's where you stand, I guess it is just a "fundamental difference of opinion". If you actually believe that you (or anyone else) is capable of being careful enough that you aren't going to accidentally write code that causes undefined behavior, then I don't think you're wrong as much as delusional.
It would (or should) crash your program in any other language anyway.
To me there is a massive difference between "there is a way a malicious user can trigger an assert, which will crash your server / webbrowser", and "there is a way a malicious user can use memory corruption to take over your server / webbrowser".
And bounds-checking penalties are small. I can tell they are small, because Rust doesn't have any kind of clever 'global way' of getting rid of them, and I've only noticed the cost showing up in profiles twice. In those cases I did very carefully verify my code, then turn off the bounds checking -- but that's fine, I'm not saying we should never bounds check, just we should do it by default. Do you have any evidence the costs are expensive?
As someone else mentioned you can boundary check access if you want to in C++ anyway and it's built in to a vector.
My point here is that it isn't really a big problem with C++, you can get what you want.
Note that this is not something C++ is able to do, and C++ has notoriously subtle iterator invalidation rules as a consequence.
Note that this is not something C++ is able to do
Or you could just not mutate the container.
They said "bounds-checking penalties are small" not that it was free. You've described a situation where bounds checking happens and declared that it "should be" expensive because it happens. You haven't given evidence that it's expensive.
You can profile it yourself, but it is the difference between direct access to cached memory (from prefetching) and two comparisons plus two branches then the memory access.
To someone who doesn't care about performance having something trivial become multiple times the cost might be acceptable (people use scripting languages for a reason), but if your time is spent looping through large amounts of data, that extra cost is a problem.
if let Some(value) = some_optional {
}
At which point value is the "dereferenced" value. Or you can take it by reference if you don't want to consume the value. auto v = std::optional<int>();
std::cout << *v << std::endl;
While both are undefined behavior, you are actually more likely to get a predictable crash from the below code than the above: int* v = nullptr;
std::cout << *v << std::endl;
I leave it to the reader to reflect on the absurdity of this.I think if you adopted a more defensive programming style where you check your values before dereferencing them, handle all your error cases, you might find C++ is not so scary. I would also recommend not using auto as it makes the types less clear.
std::optional<int> v = std::nullopt;
if (v == std::nullopt) {
return std::unexpected("Optional is empty.");
}
std::println("{}", *v);
If you are dereferencing things without checking that they can be dereferenced I don't know what to tell you.Perhaps, 3 types could exist for 3 options.
if x.is_some() {
let x_value = unsafe { x.unwrap_unchecked() };
}
everywhere.In Rust, they made the safe way of writing things easy (just write v[x]), and the unsafe one hard (wrap your code in 'unsafe'). C++ is the opposite, it's always more code, and less standard (I don't think I've seen a single C++ tutorial, or book, use .at() as standard rather than []), to do bounds checked.
So, you can write safe code in both, and unsafe code in both, but they clearly have a default they push you towards. While I think C++'s was fine 20 years ago, I feel nowadays languages should push developers to write safer code wherever possible, and while sometimes you need an escape hatch (I've used unsafe in a very hot inner loop in Rust a couple of times), safer defaults are better.
int a = v[-100]; // yolo a is set to 0
I really suspect that unlike the RISC mental model compiler writers think in terms of a superscalar processor would barely be slowed down. That is if the compiler doesn't delete the checks because it can prove they aren't needed.Essentially, the warts are too large and numerous for me to find any inner beauty in it any more.
I definitely got bit by it multiple times.
>The pimpl idiom: not sure why it's really a problem.
Because it's even more boilerplate and adds indirection in a place where you might have been painstakingly trying to avoid it (where the first indirection costs you all your cache coherency). C++ lets you go out of your way to avoid objects having to carry any overhead for virtual dispatch if you aren't going to use it; but then you might have to choose between rerouting everything through a smart pointer anyway or suffering inordinately long compile times. Nothing to do with type erasure (if I'm thinking clearly, anyway).
C++ modules are supposedly meant to save the day. Although only recently have the big three compilers (GCC, Clang, MSVC) reached some form of parity when compiling modules.
You can blame devs instead of tools all day long, but you can't deny that there are things about the tools that hold the developers back.