STX – C++17 and C++ 20 error-handling and utility extensions
lamarrr.github.ioWow. Have you looked at the sources[1]? Doc comments to the hilt. Links to `cppreference.com`. The works. If only all sources I see were commented like this. What I see instead are barren wastelands. If I find a comment every 1000 lines or so, it's completely useless and just repeats what the code says anyways. And developers who are against comments because it could be work to keep them up to date. This project is a rare respite from my daily tortures.
I was impressed as well!
Are there benefits of using this Optional and Result type over std::optional and std::expected if one is already using C++23?
The drawback I see of using a third-party solution for this is interoperability with other libraries. Rust's Optional and Result are especially great because they are part of the standard library, thus almost every project uses the same Result type. No need to define conversion operators.
> they are part of the standard library
They're part of Rust's core library. So they're available even when the full standard library isn't. Indeed core::option::Option is in effect a Lang Item, library components which are required to exist by the Rust language itself, as somebody pointed out to me on HN previously (technically Some and None are Lang Items, but well, they need to be the same type, so while an imaginary Rust implementation could name it Maybe or something instead of Option, that type needs to exist or the language can't happen at all)
C++ freestanding a) is barely supported, it's completely normal for a compiler vendor, e.g. Microsoft, to just decide they will not offer this at all and b) full of holes, so e.g. std::optional isn't provided in the freestanding library.
As a result, there isn't this same ubiquity in C++ for low level work. The language's built in types are garbage, and most of the standard library isn't available. Historically you had no choice, it's this rubbish or nothing, but Rust and to some extent Zig make that no longer true for a gradually increasing range of targets. Some C++ people have woken up and tried to improve freestanding because "We're terrible but there's no other choice" stops being a good sales pitch once your users have other Options. So I expect C++ 26 freestanding will be more useful and perhaps even better supported, for whatever that's worth.
stx::backtrace looks quite nice.
The documentation seems nice as well...except I can't seem to find a link to the code itself. I have to believe it's right in front of me and I'm just not seeing it.
> stx::backtrace looks quite nice
it's just a small wrapper around absl. Overal, the library just seems to reimplement or wrap a lot of stuff for no apparent reason
source code: https://github.com/lamarrr/stx
you aren't crazy, I also thought it was very weird that there weren't any links
This is essentially "Rust's error handling in C++", no? I generally think this is an improvement and good to have, but I'm wondering if there are any hidden differences.
The C++ standard library already exposes a std::optional type. Does this stx::Option type differ from that one?
> The C++ standard library already exposes a std::optional type. Does this stx::Option type differ from that one?
std::optional is a stack pointer, like other C++ pointers you can straight up deref’ it and get an UB.
It looks like stx::Option is an actually option type, it focuses on safety and will throw if “unsafe” methods are called on the wrong state. It also provides a slew of monadic operators to operate over values.
std::optional is not a pointer. And it has a safe dereference method called value() if you want the extra runtime check which operator* skips.
C++ got this wrong, again. The default usage should be safe, with an escape hatch, i.e. deref operator should be safe and value() should be unsafe.
Ideally, a sufficiently smart compiler would be able to see code like
And elide the double safety check, but because optional is a library feature not a language feature, the compiler needs to detect general usages of that pattern rather than specifically optimising for a language level construct. That's another place c++ is going in the wrong direction in...if (auto t = get_optional()) { do_something(*t); }The default option is high performance and less verbose.
The default option is dangerous and vulnerable being incorrect unless you explicitly use it in a different way. This is a symptom of it being a library and not a language feature.
As another example, imagine if span was a language feature. A compiler could bounds check at compile time, and fully elide the checks at runtime in many scenarios (like in a loop over the span).
A pointer with an opt-in, less convenient, safe dereference... is still a pointer. You could add a `value` observer to unique_ptr or shared_ptr.
The entire point of optional is that you can swap it in for a raw or unique pointer and it'll be cheaper because no allocation. That's not the use case for an option type.
It's not a pointer. `std::optional<T>` is a class that directly contains a `T` (within a union) and happens to offer `operator*` and `operator->` providing an API similar to a smart pointer. This does not make it a pointer (there's nothing else it could be pointing to).
Also, in an ideal world, stdlib implementations would support error-checking for `std::optional::operator*`. "Undefined behavior" just means the standard doesn't specify what should happen. Both "let's use it for unsafe optimizations" and "trigger assertion failures" are valid implementions of undefined behavior, and a good implemention should allow the user to make this choice. For gcc, see -D_GLIBCXX_ASSERTIONS. Though I'll say that it's unfortunate that C++ implementations tend to default to "unsafe optimizations" and that the opt-in to safety is not standardized.
> and will throw if “unsafe” methods are called on the wrong state
I haven’t looked at the code but the stx documentation said it doesn’t use exceptions.
The doc states it panics, maybe that's process termination, but either way it doesn't let you do the thing.
operator*() is UB on a missing optional, but ::value() exists if you want safety built into the call.
C++ also has `std::expected<T,E>`.
Thanks, TIL.
what does 'no-std' mean in STX's description? new to me, very interesting.
Yeah, I've only seen that term used in Rust, though the concept is very common in C++.
They explain it there: "No RTTI, memory allocation, nor exceptions."
Basically, nothing that has significant "runtime" cost....dynamic_cast, new/malloc, throwing exceptions.
This is generally known as "freestanding" in the C/C++ world -- https://en.cppreference.com/w/cpp/freestanding
In Rust there's actually deliberately a core library, and so no_std actually makes sense - you get the core library, not the larger std which re-exports all of core plus a lot more. There's quite a lot of stuff in that core library, just nothing that requires an operating system or an allocator. So Rust's Mutex isn't available (how can we provide a mutual exclusion mechanism on the bare hardware?) but Rust's Option<(core::net::IpAddr, core::time::Duration) is available everywhere, it's either None or a pair of an IP (v4 or v6) address and a duration, perhaps some sort of address lease because maybe we're an embedded device which has, or gives out, address leases.
In C++ this makes less sense because their standard library does have a defined "freestanding" subset but it doesn't make a very coherent whole, it's roughly the bits that seemed obviously to just not need any other components to work. It changes from version to version. And this stx library replaces what you might think of as core ideas, like an optional type, so you don't keep the shared vocabulary benefit.
[Edited to correct "standalone" to "freestanding"]
> how can we provide a mutual exclusion mechanism on the bare hardware?
This is a bit misleading. It's easier to provide mutual exclusion on bare hardware than in an OS; just do a spinlock, except with modern ISAs the core doesn't even spin.
Presumably core doesn't offer this because it's a massive footgun for devs new to multithreading who would not understand why this is unacceptable to use in an OS-hosted process.
> with modern ISAs the core doesn't even spin.
Although tricks like PAUSE are much cheaper than a naive spinlock, they are still spinning as I understand it, just not as frantically because that's pointless and wasteful.
It's even simpler than that, PAUSE just prevents the CPU from trying to speculatively execute across iterations.
I'm not familiar with a pause instruction in a modern isa. Sounds like x86 crap.
I'm talking about wfi, wfe, and friends.
IMO the typical use case is for portability to platforms that don't have those features, or implementing a lower-layer: bootloader/OS kernel/etc. Regardless of the runtime cost - you don't have a C/C++ library to rely on (or you're implementing one).
I'd always assumed rust may have taken this from C compiler arguments like -nostdlib / -nostdinc (and corresponding C++ ones -nostdlib++, -nostdinc++).
This looks amazing, thank you for sharing.