4 ms·
> Unfortunately, std::optional implements operator* and operator-> which are UB if the optional is null -- that's even worse than the situation with pointers,
by CyberRabbi 6y ago
> Unfortunately, std::optional implements operator* and operator-> which are UB if the optional is null -- that's even worse than the situation with pointers,
Dereferencing null optionals is UB for consistency with dereferencing pointers. All uses of operator* should have the same semantics and the C++ standards committee did the right thing by ensuring that with optionals. Checking for null in operator* would break consistency.
If you want to dereference an optional that may be null, use the .value_or() method. For the times when you absolutely know the optional has a value use operator*.
If you’re wondering why you would use an optional over a pointer. The idea is that optionals allow you to pass optional data by value. Previously if you wanted to pass optional data, you’d have to do it by reference with a pointer. This is part of c++’s push towards a value-based style, which is more amenable to optimization and more efficient in general for small structs (avoiding the heap, direct access of data). Move semantics are a part of that same push.
- gpderetta 6y agowell, representing an optional as a pointer, while nice syntactic sugar, is itself a design decision and adding UB there is not nice. In practice I believe that all standard libraries have a lightweight check mode that can assert even in release mode.
- kentonv 6y ago> The idea is that optionals allow you to pass optional data by value. Yes, and kj::Maybe was doing the same before std::optional was standardized. It's disappointing that the committee chose only to solve this problem while not also solving the problem of forgetting to check for null -- often called "the billion-dollar mistake". > Dereferencing null optionals is UB for consistency with dereferencing pointers. All uses of operator* should have the same semantics My argument is that `std::optional` should not have an operator* at all. `kj::Maybe` does not have operator* nor `operator->`. > If you want to dereference an optional that may be null, use the .value_or() method. For the times when you absolutely know the optional has a value use operator*. This is putting a lot of cognitive load on the developer. They must remember which of their variables are optionals, in order to remember whether they need to check for nullness. The fact that they use the same syntax to dereference makes it very easy to get wrong. This is especilaly true in modern IDEs where developers may be relying on auto-complete. If I don't remember the type of `foo`, I'm likely to write `foo->` and look at the list of auto-complete options, then choose one, without ever realizing that `foo` is an optional that needs to be checked for null. In KJ, you MUST write: KJ_IF_MAYBE(value, maybeValue) { use(value); } else { handleNull(); } Or if you're really sure the maybe is non-null, you can write: use(KJ_ASSERT_NONNULL(maybeValue)); This does a runtime check and throws an exception if the value is null. But more importantly, it makes it really clear to both the writer and the reader that as assumption is being made.
- CyberRabbi 6y ago> My argument is that `std::optional` should not have an operator* at all. `kj::Maybe` does not have operator* nor `operator->`. That’s fair. >> If you want to dereference an optional that may be null, use the .value_or() method. For the times when you absolutely know the optional has a value use operator > This is putting a lot of cognitive load on the developer. They must remember which of their variables are optionals, Not really. Teams with programmers that are bad at keeping track of the state of their variables can simply have a policy to always use .value_or()/.value() C++ doesn’t impose this on its users because it generally assumes its users are responsible enough to make their own policy. > The fact that they use the same syntax to dereference makes it very easy to get wrong. I disagree, the operator* has the same semantics as pointers did, making it no more semantically hazardous. There exists other methods on optional that have the behavior you want.
- kentonv 6y ago> always use .value_or()/.value() But neither of these solve the problem either. Neither one forces the programmer to really confront the possibility of nullness and write the appropriate if/else block. Throwing an exception rather than crashing is only a slight improvement IMO. > I disagree, the operator* has the same semantics as pointers did, making it no more semantically hazardous. It was already severely hazardous with pointers, that's the problem.
- CyberRabbi 6y ago>> always use .value_or()/.value() > But neither of these solve the problem either. Neither one forces the programmer to really confront the possibility of nullness and write the appropriate if/else block. .value_or() actually does and you can certainly add a lint check against dereferencing optional or using .value() if you’d like. C++ does not Yet provide Case-style syntax for handling variants like rust, outside of macros and the standard library will certainly not define macros. I think what you have done for your codebase makes sense based on your preferences but I think the standard optional works pretty well given the variety of code based and styles it’s intended to support. > It was already severely hazardous with pointers, that's the problem. So then don’t use the dereference operator.
- ori_b 6y ago> Dereferencing null optionals is UB for consistency with dereferencing pointers The point of optional is to avoid being consistent with the bad parts of pointers. And making it undefined rather than a guaranteed crash is even crazier. Ford doesn't sell cars that burst into flames for consistency with the Pinto.
- CyberRabbi 6y agoAnd usage of the dereference operator isn’t intended for uses that would cause things to burst into flames. If you don’t know the state of your variables or you don’t trust your coworkers to know the state of their variables, you can enforce the use of value_or() in your own projects. You don’t get to force superfluous branch stalls on the general C++ user base.
- emtel 6y agoI think your replies in this thread show a complete misunderstanding of what std::optional is for (or at least, what it should be for, in my opinion). std::optional is for modeling a value that may be null. If the value may be null then you must check if it is null before you dereference it. There is no "forcing of branch stalls", because if used correctly (and designed correctly, which std::optional is not, sadly) it is merely a way for the programmer to use the type system to enforce the use of null checks that are necessary for the correctness of the program anyway. If you and your coworkers find yourself in a situation in which you know that the value of a pointer cannot be null, then you should not model that value with an optional type, and then you will not only not be required to add a null check, it will be immediately obvious that you don't need one.
- CyberRabbi 6y ago> If you and your coworkers find yourself in a situation in which you know that the value of a pointer cannot be null, then you should not model that value with an optional type, and then you will not only not be required to add a null check, it will be immediately obvious that you don't need one. Hmm I think you’re suffering from a lack of imagination and real world experience with efficiently storing data in C++. There are certainly cases where it makes the most sense to instantiate your value within an optional wrapper while at the same time there being instances within your codebase where that location is known to be non-null. I’m surprised I even have to say that. An obvious case is when using optional as a global. Other cases are when you’ve checked the optional once at the top of a basic block to be used multiple times after.
- beached_whale 6y agooptional has a bunch of reason's why it is better than a pointer too(it cannot be incremented, it's not a failure to initialize, it doesn't implicitly convert to things readily...). Unfortunately we don't have a monadic optional or an optional with a reference member. Both would be very useful. Value or suffers from having to evaluate the or part in all cases, but if we had .transform/.and_then/.or_else members would be really nice. Optional of a reference would allow for a whole swath of try_ methods instead of the idiomatic at( ) interface for checking bounds and retrieving in one call. at( ) suffers that it forces the choice of an exception or checking the index for being in range outside the call and then operator[] is what you want.
- CyberRabbi 6y agoSome monadic method would be nice but as you probably know, it’s trivial to implement one yourself. You can store a reference in optional using reference_wrapper https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper https://en.cppreference.com/w/cpp/utility/functional/referen...
- beached_whale 6y agoreference wrapper breaks in generic contexts.
- CyberRabbi 6y agoCould you provide an example?
- clappski 6y agoYou have to add special handling for the case that T == std::reference_wrapper<T> so you can call .get() on it to expose the underlying value. In the case of std::optional vs a pointer type (raw or smart) you can consistently use operator* to get to the underlying value. I think this is what was meant.