4 ms·
UB-invoking dereference in std::optional is such a baffling design choice. The whole point of an optional type is to prevent accidental unchecked access to the
by pornel 6y ago
UB-invoking dereference in std::optional is such a baffling design choice.
The whole point of an optional type is to prevent accidental unchecked access to the value. Sure, sometimes it's useful for performance to skip the check when it's already known-safe from the context, but such dangerous optimization should have been behind a method like `beware_of_the_nasal_demons()`, not an innocent-looking convenience syntax for flirting with the UB — in a language that was supposed to be cleaning up the unsafety.
- dataflow 6y ago> The whole point of an optional type is to prevent accidental unchecked access to the value. You might argue it should be that, but it wasn't my impression that it is that. My impression was std::optional is just there to allow for representing the absence of a value. Ideally that should be zero-overhead, which means dereferencing it should degenerate into a normal object access. Hence the current design.
- nemothekid 6y ago>Ideally that should be zero-overhead Optionals are not zero-overhead in any other language. If you aren't going to bother checking the optional, why not just return any old pointer type? The type system should be used to enforce a contract and bypassing checks on that feels like you are just spitting in the fact of the type checker.
- dataflow 6y ago> Optionals are not zero-overhead in any other language. That's been true for so many other features in C++ too. > If you aren't going to bother checking the optional, why not just return any old pointer type? Pointers require something to point to. Optionals can embed the value. > The type system should be used to enforce a contract and bypassing checks on that feels like you are just spitting in the fact of the type checker. It's bypassing something alright, but I wouldn't say that's the type checker. And I mean, you could use this logic for everything else. Iterators, arrays, etc. should all have bounds checking too, right? If you want that, there's C#...
- lmm 6y ago> And I mean, you could use this logic for everything else. Iterators, arrays, etc. should all have bounds checking too, right? If you want that, there's C#... I mean yes, the logical conclusion is that trying to backport safety onto C++ is impossible and moving to another language is the only reasonable option. That's kind of the point of the article.
- dataflow 6y ago> I mean yes, the logical conclusion is that trying to backport safety onto C++ is impossible The logical conclusion is that what some people here seem to want is so radically different from C++ that they really just want an inherently just a different language altogether... which is fine. Nothing about this implies it's impossible to shoehorn C++ into something else (though that may still be true; I'm not sure). It just implies that another language should be considered when you want to prioritize memory safety, like maybe C# or Rust. (And nowhere here am I agreeing or disagreeing with the article.)
- nemothekid 6y ago>That's been true for so many other features in C++ too. Plenty of C++ features are zero-cost, not necessarily zero-overhead. To me having an optional without bounds checking is like obtaining a shared_ptr without incrementing it's use_count. It's like arguing that I should increment use_count manually so that copies are "zero-overhead". Sure it's "faster" but the design is still broken and will lead to all sorts of issues. >Pointers require something to point to. Optionals can embed the value. So? Just because it embeds a value rather than a pointer doesn't make your program any more correct. If the API designer returned an empty optional and you access it anyways you are still dealing with garbage data. >And I mean, you could use this logic for everything else. Iterators, arrays, etc. should all have bounds checking too, right? If you want that, there's C#... Ok? Then lets remove all overhead from the language. Why does shared_ptr increment it's refcount behind my back? No more shared_ptr. This kind of argument without nuance gets you nowhere. Likewise, unlike bounds checks, nothing about std::optional is forced or baked into the language. If you don't want to do null checking, then just don't use std::optional - the fast option is still there and unlike Rust, the fast option is the default. Optionals are a great tool to eliminate null pointers from a codebase. It's a big enough problem that I would expect an optional type, designed in 2018 to get it right. Allowing automatic dereferencing is an oversight.
- matvore 6y ago> why not just return any old pointer type? Using a pointer type in a straightforward manner would entail allocating the object on the heap. If allocated on the stack it has obvious lifetime limitations.
- emtel 6y ago> Optionals are not zero-overhead in any other language. This is a common misconception, and a pet peeve of mine. Properly designed optional types add neither space nor code-size overhead. Consider the C function: foo(int* ptr) { ... } If it is part of the API of foo that `ptr` may be null, then foo must not dereference `ptr` without verifying that it is non-null. So the body of foo must be something like: foo(int* ptr) { if (ptr != NULL) { ... } } Or, take the non-pointer case: foo(struct S s, bool s_present) { ... } Clearly the intent of this function is that the contents of `s` should not be accessed if `s_present` is false, so the implementation of the function must check `s_present` before inspecting `s`. (Inspecting s incorrectly wouldn't necessarily be UB, like in the previous example, but it's clearly erroneous) The only thing that a good optional type implementation does is make it a type error to fail to do what everyone agrees both of those functions must do anyway in order to be correct. It need not increase the size of the representations, nor must it emit a single unnecessary instruction when compiled. There are plenty of languages that do this, Rust being a prominent example.
- jcelerier 6y ago> If it is part of the API of foo that `ptr` may be null, then foo must not dereference `ptr` without verifying that it is non-null. So the body of foo must be something like: foo(int* ptr) { if (ptr != NULL) { ... } } then what ? foo(int* ptr) { if (ptr != NULL) { *ptr += 1; // optional<T>::operator* would introduce a branch here ? *ptr = *ptr / 2 ; // same if(*ptr > some_constant) // same { ... } // etc etc } } and compiler optimizations can't always be assumed, for instance debug mode (-O0) performance does matter
- emtel 6y agoThat’s why I said properly implemented! Good optional implementations let you do the check once to unwrap the inner value, so you have one branch. In rust: fn foo(s: Option<&S>) -> { if (let Some(inner) = s) { // now you can access inner repeatedly // without branches } } (edit to fix syntax errors)
- kragen 6y agoDo you mean that the purpose of std::optional is to be faster than a pointer (which could represent absence as nullptr)? I would have thought it would be ML-style safety, but it seems like you're right. I guess a tagged union (or std::pair<T, bool>) would be faster—if the std::pair is a local variable, fields of the wrapped object will be at fixed offsets from your stack pointer or frame pointer, so you save yourself an indexed register load the first time you access a field of the object. That seems to have been the intended use. Why did they think this was a good idea?
- dataflow 6y agoNo, the purpose wasn't to be faster than a pointer. You can't even necessarily use a pointer where you can use std::optional; the value of std::optional is embedded in itself. One purpose would be type safety (and things revolving around that). Another would be to let you pass around an optional object just as easily as the underlying object. As to whether it's a good idea to have it at all, I'm not particularly fond of it personally either. I don't have specific issues with its dereferencing safety though. Don't confuse type safety with memory safety though. Type safety != memory safety != thread safety != ...
- kragen 6y agoAll good points. Thank you.
- bnurun 6y ago> not an innocent-looking convenience syntax for flirting with the UB I agree, but C++ uses this pattern in so many places that I think it is less confusing to be consistent. When I see a nice, concise syntax or function, I check cppreference for undefined behavior...
- account4mypc 6y agoboost::optional was designed to mimic pointers. Before boost::optional, some programmers would return a T* as a caveman's optional<T>. Boost wanted to preserve the syntax of *, ->, and operator bool. Dereferencing a null ptr is UB, so operator* for optional was the same. I agree it would be nice if you could get an assert in operator*. But you can already fire up a debugger or sanitize build and get a nice error message anyway, so it's nbd.
- Someone 6y ago> it would be nice if you could get an assert in operator* If, as you wrote, operator* for an optional that doesn’t contain a value is UB, it can do whatever it wants, including assert. I think the problem is that there’s an implicit requirement that operator* optionals that do contain a value is as fast as a pointer dereference. (Aside: reading https://en.cppreference.com/w/cpp/utility/optional https://en.cppreference.com/w/cpp/utility/optional, I wonder how one can misuse “When an object of type optional<T> is contextually converted to bool, the conversion returns true if the object contains a value and false if it does not contain a value.” to write obfuscated code or hide back doors using optional<bool>)
- account4mypc 6y agowell, the immediate problem is it's hard to rewrite the STL without resorting to ugly patches
- solipsism 6y agoEasily dealt with with a linter. C++ is not fundamentally a safe language. It's fundamentally a no-cost abstraction language. It's not a baffling design choice if you know C++.
- pjmlp 6y agoPity that linters isn't something most C++ devs feel like they need. https://isocpp.org/files/papers/CppDevSurvey-2021-04-summary.pdf https://isocpp.org/files/papers/CppDevSurvey-2021-04-summary...
- ttt0 6y agoAs far as I'm considered, the purpose of std::optional is so you don't have to allocate memory on the heap and then check for null pointer. I don't want it to throw exceptions, just like I don't want the language to check the validity of a pointer every time I want to dereference it.
- jcelerier 6y ago> The whole point of an optional type is to prevent accidental unchecked access to the value no ? it is to model the idea of "a value is there or is not there". definitely nothing more. If replacing a T* t by a optional<T>& incurs a meaningful performance cost (like a branch) then optional just won't be used