A decent option type for C++

I'm stuck in C++ for a while. The C++ 17 standard added std::optional but it's ... a little disappointing.

I'm resisting the temptation to try to implement something better myself. Is there a library out there, providing an optional type for C++ that is more useful than std::optional?

Can you elaborate on what exactly you are looking for?

If you want a Rust-style 100% safe optional, you won't get it. You won't be able to eliminate the related problems (eg. null pointers) by adding more types.

At the very least I would like it not to have operators * and -> with undefined behaviour when used on the empty variant.

It would be good if it had some methods for operating on the contents. I see that and_then, transform and or_else are slated for inclusion in C++23, which is a start, and the odds of compilers actually implementing them before around 2027 shouldn't be too bad.

std::option does not admit references, I haven't thought about the implications in terms of implementation and usage, but I imagine that this could lead to more frustration.

More generally speaking, I am sure that the issues which make it difficult to create (and use) a decent option type in C++ are legion, and I'm hoping that someone, somewhere has thought about this from a Haskelly/Rusty perspective. A quick search has mostly turned up discussions of std::optional from a C++ perspective and, to me, they all sound like "std::option is crap, and this is good because we like our utilities to be crap in C++" or "if it were any better, then anyone would be able to write correct programs in C++ and that threatens my ego and/or job security".

std::variant<None, T>

Hmm, yes.

The context is that we are writing a wrapper library that is supposed to provide more convenient, simpler, easier to understand, higher-level and safer interfaces to a scientific framework, with the aim of allowing non-expert programmers to write code that is easier to write, read, understand and maintain.

I don't think that std::variant is really compatible with these goals, at least if exposed directly, and if it's not exposed directly, then I'm back to rolling my own, or looking for prior art.

That's not the problem of std::option but references. References in C++ aren't first-class types. They are very hard to use except in the most trivial case of declaring a local variable or a function parameter.

I'm no C++ guru but since C++11 one has been able to use the delete specifier to disable the usage of a deleted function. IBM Documentation

Presumably one could derive ones own option type from std::optional with the * and -> operators deleted.

Or just override * and -> with something you like better.

From a practical standpoint, I think you should probably just use std::optional.

If you wanted to avoid classes that have dangerous methods, you'd also have to avoid std::array and std::vector -- both of them expose indexing operators that have undefined behavior when misused.

It's just how C++ operates, basically.

3 Likes

OK, let me try a different angle: I want something that makes it impossible for the user to derefencence a null pointer by being unaware (or forgetting) that the pointer might be null.

I am perfectly aware of that.

This does not mean that you cannot devise APIs which mitigate it.

There are very many things very seriously wrong with the framework I (and my users) are obliged to use, and the language in which it is written. This whole project is an attempt to protect users from the things that they can do wrong through their ignorance (or forgetfulness) of the bazillion details you have to be aware of when using this framework and this language. The whole project is founded on the belief that it is possible to do better, even if it is impossible to remove all problems.

std::option just doesn't provide enough benefits to we worthwhile exposing to clients.

I'm curious how you would design it. Imagine if std::optional was behind a wrapper struct whose only interface was is_none() and an unwrap() that aborted when null. It would be less efficient, requiring unwrap to copy T out, for instance. And that won't be possible if it has any self-references or if other references to the inner value somehow exist. Maybe it could work, though.

The key insight is that you will very likely have to give up on some "zero cost abstraction" to avoid many of the pitfalls in the language.

1 Like

Um, no. It could very well return a pointer or a reference that is always valid or abort/throw.

I'm perfectly happy to give up zero cost abstractions in many (which does not mean all) places in my code. Sometimes correctness is much more important than saving CPU cycles, and the whole C++ philosophy fails to grasp this very important point.

Also, a comment about the term 'zero-cost':

  • this all refers to runtime: the costs of most of C++'s 'zero-cost abstractions' are huge at development time, debugging time, retraction-of-papers-published-on-the-basis-of-results-produced-by-buggy-code time, etc.
  • even at runtime, there are indirect costs, resulting from naive programmers doing convoluted and sub-optimal things as a consequence of the trainwrecks that usually arise in C++ when two or more features of the language collide.

TLDR: there are plenty of places where correctness is more important than saving CPU cycles, and C++ is miserably unsuitable in such situations. (i.e. C++ is not fit for purpose as a general programming language.)

You can find a "safe" Option implementation here.

1 Like

While this is not a very practical suggestion because the library is upfront about being an experimental proving ground that is not stable, the subspace library's goal is to make safer C++ abstractions like what you're looking for. Here is the documentation and implementation of the subspace's take on the Option type. It has some desirable properties that Rust Options share like the niche-optimization such that the size of Optional<&T> equals the size of &T, etc while also panicking on * and -> instead of UB and having a lot of the nice transformation helpers that you'd expect in Rust.

Once again, the library explicitly advises you against relying on it due to its experimental and unstable nature but you can probably consider this implementation as a proof of concept for what you can achieve in C++ with respect to achieving something like Rust has. Its also a very interesting project that I think deserves some visibility :slight_smile: .

3 Likes

Yes, but nothing prevents the destructor from being called while a reference is held.

You are not going to be able to that in C++. The language simply doesn't provide any facility for checking such lifetime issues.

What you can do is put the value behind a shared_ptr and only hand out copies of that. This will prevent users from destroying the pointer by destroying the Option, but it still won't protect an unsuspecting dereferencer of the pointer against destroying their own copy.

It's not an accident Rust was created. It's plainly impossible to have these sort of guarantees in C++. They can't be retrofitted to a language that wasn't designed with such problems in mind. The language surface is enormous as well as the existing library ecosystem, and changes like this would break everything if implemented properly (ie., soundly).

If you want to have this level of certainty about memory safety, you'll have to use something other than C or C++.

Precisely. My point is that if there is any unsoundness, can it be considered safe? Once a reference is exposed, C++ doesn't provide any guarantee that all uses of it will be well defined. You can sort of avoid that only by not providing a reference in the first place.

We're talking about the same thing. But this is why I was initially curious about how the implementation would be proposed. There are so many ways to make mistakes in C++ that it seems like an exercise in futility to make a safe optional type.

1 Like

Like most things in C++ it's like a huge bank vault facade on the front of a wooden garden shed. Just go round the back to get in.

1 Like

std::optional has a "safe" access method: std::optional::value() throws an exception if there is no value.

That's almost equally unsafe. What if the user forgets to assign a value to a shared_ptr variable that was supposed to contain the value, and dereferences that? Undefined behavior.

I agree with @parasyte it seems like an exercise in futility. You'd have to replace the whole standard library. C++ is designed around not having to check for errors at runtime.

I get it if you don't like that, but then you shouldn't really be providing that API in C++. Write a Rust wrapper!

If you're trying to put a reference into something in C++ and it's not working, you want std::reference_wrapper - cppreference.com

This is not how C++ works. You must always be aware of the restrictions, and you cannot ever forget them.

At best you have conventions, like using C++ references only in stack discipline so that you can pretend that they're always valid. But there's nothing that keep people from giving you bad references; they have to remember every single time.

This is why I find Rust so freeing, despite that being counter-intuitive to those who hear things like "fighting the borrow checker". When I'm not using unsafe, I don't have to be perfect. The compiler has my back.

7 Likes