Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

std::optional is unsafe in idiomatic use cases? I'd like to challenge that.

Seems like the daily anti c++ post



I’m very much pro c++, but anti c++’s direction.

> optional is unsafe in idiomatic use cases? I’d like to challenge that.

    std::optional<int> x(std::nullopt);
    int val = *x;

Optional is by default unsafe - the above code is UB.


A static analyzer could easily tell you to use the monadic member functions instead of a raw dereference.


But using the deref op is deliberately unsafe, and never used without a check in practice. This would neither pass a review, nor static analysis.


> This would neither pass a review, nor static analysis

I beg to differ. Humans are fallible. Static analysis of C++ cannot catch all cases and humans will often accept a change that passes the analyses.


> Static analysis of C++ cannot catch all cases

You're ignoring how static analysis can be made to err on the side of safety rather than promiscuity.

Specifically, for optional dereferencing, static analysis can be made to disallow it unless it can prove the optional has a value.


GP picked the less useful of the two examples. The other one is a use-after-move, which static analysis won't catch beyond trivial cases where the relevant code is inside function scope.

I also agree with them: I am pro-C++ too, but the current standard is a fucking mess. Go and look at modules if you haven't, for example (don't).


No type is safe to use after a move, unless it's documented to put itself in to a well defined state and says as such

You can't magically make all the member functions on std::vector safe after a move for example unless the moved from vector allocates itself a new (empty) buffer, which kills the performance benefits.

It's all by design.


I believe it's actually the opposite. You're supposed to be able to reuse objects that were moved from unless otherwise documented, although it may require reinitializing it explicitly. A moved from vector is valid to reuse. Although the standard doesn't specify what state it will be in, all major standard library implementations return it to the default constructed state which doesn't require an allocation.


Read what I wrote again. I said all member functions.

Reusing of a moved from object only requires assignment and destruction to be well behaved.

The std library containers give you extra guarantees (a moved from object is effectively the same as a default constructed one), but the _language_ imposes no such requirements on your types

It's perfectly allowed by the language for the .size() member of your own vector type to return a random value after it's been moved from because you wanted to save 1 CPU instruction somewhere.


I understand what you're saying but no major compiler does anything nefarious. They default initialize it. A lot of code depends on std::optional that was moved from returning false for is_valid for instance and no one would break that even if it's not guaranteed.


This is false. Moved-from is a "valid but unspecified" state in C++, so perfectly safe, but each type must decide what to do. At the minimum, the destructor must be able to run, because it will be invoked, meaning that the obvious choice is also the only sensible one: letting moved-from be equivalent to the "empty" state.

An empty std::vector does not require that any buffer is allocated. It just has a null data pointer.


See my sibling comment regarding vector.

It's not true that the only sensible choice for a moved-from object be equivalent to the defaulted constructed one.

If your move constructor doesn't exist then the copy constructor gets called under the language rules, so the sensible default is actually a copy.

Everything else is an optimisation that has a trade-off

A conforming implementation of std::list, for example, can have a default constructor and a move constructor that both allocate a sentinel node on the heap, which is why none of the constructors are noexcept.

If you don't allocate a sentinel on the heap, then moving std::list can invalidate iterators (which is what GNU stdlibc++'s implementation chooses).

It's a trade off.


I did not say “default-constructed”, because that’s a whole other can of worms.

But yes, the implication of C++ move semantics is that every movable object must also define an “empty” (moved-from) state, so you cannot have something like a never-null unique ptr.

Specifically, it is not allowed for the moved-from object to be inconsistent or to say “using it in any way is UB”, because its destructor will run.


> never used without a check in practice

Ho ho ho good one.


That is actually memory safe, as null will always trigger access violation..

Anyway safety checked modes are sufficient for many programs, this article claims otherwise but then contradicts itself by showing that they caught most issues using .. safety checked modes.


It is undefined behavior. You cannot make a claim about what it will always do.


As a fun example, I worked on a safety-critical system where accessing all-bits-zero pointers would trigger an IRQ that jumped back to PC + 4, leaving the register/variable uninitialized. Great fun was had any time there was LR corruption and CPU started executing whatever happened to be next in memory after function return.


Hahahaha well that behaviour is certainly fun!

I recently had a less wild but similarly baffling experience on an embedded-but-not-small device. Address 0 was actually a valid address. We were getting a HardFault because a device driver was dereferencing a pointer to an invalid but not-null address. Working backwards, I found that it was getting that invalid address not from 0x0 but rather from 0xC… because the pointer was stored in the third field of a struct and our pointer to that struct was null.

   foo->bar->baz->zap
Foo = 0, &bar = 0xC, baz = invalid address, *baz to get zap is what blew up.


>null will always trigger access violation..

No, it won't. https://gcc.godbolt.org/z/Mz8sqKvad


Oh my bad, I read that as nullptr, I use a custom optional that does not support such a silly mode as "disengaged"


How is that an optional then?

The problem is not nullopt, but that the client code can simply dereference the optional instead of being forced to pattern-match. And the next problem, like the other guy mentioned above, is that you cannot make any claims about what will happen when you do so because the standard just says "UB". Other languages like Haskell also have things like fromJust, but at least the behaviour is well-defined when the value is Nothing.


What do you return if there is no value set? That’s the entire point of optional.


You didn't read this, did you? https://alexgaynor.net/2019/apr/21/modern-c++-wont-save-us/

It's not a pointer.


Two of the authors are libc++ maintainers and members of the committee, it would be pretty odd if they were anti C++.


They linked directly to https://alexgaynor.net/2019/apr/21/modern-c++-wont-save-us/ which did exactly what I'd guessed as its example:

> The following code for example, simply returns an uninitialized value:

  #include <optional>

  int f() {
    std::optional<int> x(std::nullopt);
    return *x;
  }


But that is not idiomatic at all. Idiomatic would be too use .value()


Sadly I have lots of code that exclusively uses the dereference operator because there are older versions of macOS that shipped without support for .value(); the dereference operator was the only way to do it! To this day, if you target macOS 10.13, clang will error on use of .value(). Lots of this code is still out there because they either continue to support older macOS, or because the code hasn't been touched since.


Just a cursory search on Github should put this idea to rest. You can do a code search for std::optional and .value() and see that only about 20% of uses of std::optional make use of .value(). The overwhelming majority of uses off std::optional use * to access the value.


Not only is this a silly No True Scotsman argument, but it's also absolute nonsense. It's perfectly idiomatic to use `*some_optional`.


It is with a prior .has_value call. It's not correct without. It's simple, and covered by static analysis. This is not an issue in real code, it's a pathologic error that doesn't actually happen. Like most anti c++ examples.


It is discussed in the linked post: https://alexgaynor.net/2019/apr/21/modern-c++-wont-save-us/

tl;dr: use-after-move, or dereferencing null.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: