5 ms·
No, I'm talking about std::optional<T>. It's a type designed to store an object of type T, or nothing. You use '*v' to get the value out (if one is present). I
by CJefferson 2y ago
No, I'm talking about std::optional<T>. It's a type designed to store an object of type T, or nothing. You use '*v' to get the value out (if one is present).
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.
- CyberDildonics 2y agoIt was originally discussed as a way of avoid null pointers, 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.
- Maxatar 2y ago>I don't think this is true. I don't think it has anything to do with null pointers 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 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/p2988r0.html 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.
- CyberDildonics 2y agoI 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 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.
- Maxatar 2y agoYes you did and I literally quoted it, here it is, your words: "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.
- CJefferson 2y agoI just want to pick out one thing: 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?
- CyberDildonics 2y agoIf you profile looping through an array of pixels in linear memory order vs bounds checking every access it should be more expensive due to the branching. 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.
- Maxatar 2y agoIterating over an array in Rust does not involve bounds checking every access since Rust guarantees immutability to the container over the course of the iteration. Hence all that's needed is to access the size of the array at the start of the iteration and then it is safe to iterate over the entire array. Note that this is not something C++ is able to do, and C++ has notoriously subtle iterator invalidation rules as a consequence.
- CyberDildonics 2y agoThey asked for evidence of bounds checks being expensive, please try to keep the context in mind. Note that this is not something C++ is able to do Or you could just not mutate the container.
- carom 2y agoYou have to do a very similar operation to has_value() .value() with Rust's optional though... How is opt.ok_or("Value not found.")?; so different from the C++ type?
- Maxatar 2y agoOne is undefined behavior which may manifest as entirely unpredictable side-effects. The other has semantics that are well specified and predictable. Also while what you wrote is technically valid, it's not really idiomatic to write it the way you did in Rust, you usually write it as: 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.
- carom 2y agoI prefer ok_or for optionals and map_err for for results in Rust. I believe the way you proposed ends up with very deeply nested code which I try to avoid.
- Kranar 2y agoIt seems like people think dereferencing an unengaged optional results in a crash or exception, but ironically you are actually less likely to get a crash from an optional than you would from dereferencing an invalid pointer. This snippet of code, for example, is not going to cause a crash in most cases, it will simply return a garbage and unpredictable 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.
- carom 2y agoI understand what undefined behavior is, I just don't dereference pointers or optionals without first checking them against nullptr or nullopt (respectively). In fact, I generally use the .has_value() and .value() interface on the optional which, to my point in the above comment, is a very similar workflow to using an optional in Rust. 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.
- tangus 2y agoYou can pay the penalty and get safe container access, too. AFAIK, C++ standard containers provide both bounds checked (.at()) and unchecked ([]) element retrieval.
- CJefferson 2y agoNow we are getting down to a philosophical issue (but I think an important one). 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.
- Gibbon1 2y agoI've been wondering how much you'd pay if you just required that out of bounds accesses be a no-op with an option of calling an error handler. De-reference a null or out of bounds pointer you get zero. Write out of bounds, does nothing. 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.
- superblas 2y agoMy understanding is that the cost that is mentioned comes from the branch introduced by checking if the dereference is in-bounds (and the optimizations these checks allegedly prohibit), not from what happens if an OOB access is detected
- Gibbon1 2y agoWhere I feel that breaks down while probably leet code would be faster. But high quality code tends to have checks to make sure operations are safe. So you got branches anyways in real code.