When we added the equivalent to Rust, it was a bit controversial. But the idea of "an optional type is a list with zero or one items in it" is much more normal in functional places, and so a lot of people like it quite a bit too.
The fun thing is that the original design (N3527 over a decade ago) for std::optional specifically says it's not the container with exactly zero or one items, whereas I believe Rust's Option never made this claim and so recognising this isn't a U-turn.
I really like how some good structures used by other languages, specially Rust and Zig, have been added to the newer C++ standard.
The Result, Optional, and Variant are really sweet for day-to-day use of the language, and those in-process standard libraries of SIMD operations, BLAS mathematical functions, and the the execution library looks really cool, specially as standard.
I would like C++ would be a little more "batteries included" in some ways, like having a basic standard for signals, networking (just handling sockets would be a huge thing), and some basic system calls.
> I really like how some good structures used by other languages, specially Rust and Zig, have been added to the newer C++ standard.
The Result, Optional, and Variant are really sweet for day-to-day use of the language, and those in-process standard libraries of SIMD operations, BLAS mathematical functions, and the the execution library looks really cool, specially as standard.
for Optional and Variant they both were basically standardized versions of boost.optional & boost.variant, which exist since 2003 and 2002 respectively. Most of the time you can just change boost:: to std:: and it works exactly the same ; for many years software I develop could switch from one to another with a simple #ifdef due to platforms not supporting std::optional entirely (older macOS versions, pre 10.14 IIRC)
"I would like C++ would be a little more "batteries included" in some ways, like having a basic standard for signals, networking (just handling sockets would be a huge thing), and some basic system calls."
Besides basic handling of TCP sockets and the Unix-style "Ctrl-c" keyboard interrupt, none of the stuff you're asking for is portable across different platforms. I'm not saying it's a bad idea, just that there is no one single universal standard for what an OS should do and what knobs and levers it should expose, or at least one that everybody follows.
Linux has non-trivial deviations from the POSIX spec, and even FreeBSD and OpenBSD have deviations. POSIX has its own compliance test suite that it runs to award certification of compliance, but it's not open source and it you need to pay a fee for it.
All of that however, is a drop in the bucket compared to making an API that exposes all the knobs and levers you want in a way that behaves exactly the same on Windows which barely has any architectural resemblance to UNIX. For exmaple, NTFS is case-insensitive by default and has nothing resembling the UNIX style of file permissions. Or more importantly, signals do not exist on Windows; something resembling signals for keyboard interrupts exists, but stuff like SIGHUP and SIGBUS does not. I'm talking the kind of known caveats that come with using a POSIX-compatibility layer on Windows, e.g. Cygwin.
I think if I get much deeper than that I'm just being pedantic, but even Python code behaves differently on Windows than it does on all the POSIX-like OSes out there.
> I would like C++ would be a little more "batteries included" in some ways, like having a basic standard for signals, networking (just handling sockets would be a huge thing), and some basic system calls.
Isn't Boost library basically that? C++ has been slowly adopting freatures from it to its standard library.
That's a bit hyperbolic. Sure, it's not exactly ergonomic, but that doesn't mean I can't use it. One thing that bugs me is that there is still no overload helper in the standard:
// helper type for the visitor
template<class... Ts>
struct overloads : Ts... { using Ts::operator()...; };
I will boldly state that std::variant makes all code worse and it is always better to not use it.
I love sum types in Rust. They’re great. This stuff exists solely to add ergonomics. If it’s not ergonomic it’s just making your code worse!
std::variant is fundamentally broken because it uses types as its discriminant. Want a variant with two ints that represent two things? Fuck you, you can’t. Rust enums of course can do this.
std::visit is an abomination. As is the template overloaded bullshit you have to copy paste into projects.
And of course the error messages when you get when it’s wrong are atrocious even by C++ standards.
No. std::variant is awful and you should simply never use it. This could change in 2040 when you can use C++32. But for now just avoid it.
>Want a variant with two ints that represent two things? Fuck you, you can’t.
This is false, I also find that in general you have a tendency to make false claims about C++ in most of your posts about it. I would suggest you check out a resource like https://en.cppreference.com/ just as a sanity check before you make claims about the language in the future, and also because it's a very good resource for learning the ins and outs of the language.
As for your claim, std::variant supports index based discrimination similar to std::tuple, so you can absolutely have a std::variant<int, int>, and access the second int along the lines of:
I treat std::variant the same way I treat std::tuple, which is that I use them internally/privately and don't expose them as part of a public API.
If I want to expose a std::variant publicly then I take the effort to emulate Rust, which I think everyone agrees has an incredibly useful and elegant enum type so it looks like this:
int main() {
auto s1 = ConnectionState::Disconnected();
auto s2 = ConnectionState::Connecting(3);
auto s3 = ConnectionState::Connected("192.168.1.5", 8080);
for (const auto& state : {s1, s2, s3}) {
state.visit(
[](Disconnected) {
std::cout << "Disconnected\n";
},
[](int retries) {
std::cout << "Connecting (" << retries << " retries)\n";
},
[](const IpAddress& ip) {
std::cout << "Connected to " << ip.host << ":" << ip.port << "\n";
});
}
}
To implement that I currently do need to write out boilerplate like below, but with C++26 I will be able to use the upcoming reflection feature to automatically implement the bulk of this code by reflecting on the std::variant directly:
> I will boldly state that std::variant makes all code worse and it is always better to not use it.
And I will boldly state that your comment is again completely hyperbolic.
> std::variant is fundamentally broken because it uses types as its discriminant.
In my experience, the typical use case for std::variant is static polymorphism. For this purpose it works just fine because the type is the discriminator.
Would proper sum types and pattern matching be nice? Of course! But this this mean that std::variant is fundamentally broken? I don't think so.
out of curiosity, can you elaborate on that a bit? I've shipped std::variant in a few designs and it's been OK, but maybe I didn't use it deeply enough to see issues.
It doesn't have proper pattern matching (you have to use a visitor class or the weird overload trick), and it sucks for compile times. It should have been a language construct rather than adding it to std, but the committee is often too scared to touch the language because they are stubbornly unwilling to break backwards compatibility.
C++ is a proud New Jersey language. So the priority has been and continues to be ease of implementation. If it's tricky to make it work right, don't sweat it - the users (in this case that's us programmers) will put up with it not working right.
std::variant has valueless_by_exception - the C++ language wasn't able to ensure that your variant of a Dog or a Cat is always, in fact, a Dog or a Cat, in some cases it's neither so... here's valueless_by_exception instead, sorry.
Ease of implementation was not the issue here, on the contrary the easier implementation would have guaranteed that std::variant<Dog, Cat> always contains a Dog or a Cat. The issue was performance in that guaranteeing a Dog or a Cat requires dynamic storage duration if either of Dog or Cat has a throwing constructor.
If neither Dog or Cat have throwing constructors, then std::variant<Dog, Cat> is guaranteed to always be a Dog or a Cat.
Herb Sutter calls this kind of thing "spelling things generically" meaning that the same generic code will compile against different types.
In this case the same for loop code will compile against a type that may hold zero or one items and a type that may contain 0->n items. Maybe this pattern could be extended to shared_ptr for example.
It's sort of like a cleaner approach to a C# SelectMany where you return either an empty array or a single-item array?
C++ keeps getting more stuff but it's clear a lot of thought goes into these additions. I think a "living" language has proven better over time (C# evolved heavily and borrowed from other languages, C++ does as well, maybe lately concepts from functional and Rust?) Go might be the big exception that evolves super-slowly.
The for syntax over optional seems unnatural. I get that optional is like a list of 0 or 1 owns. Is there a fault in my reading that makes the syntax clunky?
Well, you can map over options/maybes. For-loop style iteration over an option does seem a little strange IMO, first, because syntactically it looks like overkill, and second (and more importantly), because map evals to a value, while `for` does not.
But I suppose in C++, given the centrality of looping constructs, this is what you would do to accommodate option.
bent my brain for a moment. `logger` is a list of loggers to send data do? Oh, no, it's either 0 or 1 loggers.
Rust's `if let` syntax maps much more closely to how I think about it. I guess if this becomes idiomatic C++ then it'd start looking perfectly normal to everyone, but it still seems odd. For instance, I don't think I could ever bring myself to write Python like this, even if it worked:
def do_something(data, logger=None):
for l in logger:
l.log(data)
That makes sense! What's funny is, I went to go write the equivalent Rust code:
struct Logger;
impl Logger {
fn log(&self, _: &str) {}
}
fn do_something(data: &str, logger: Option<Logger>) {
for l in logger {
l.log(data)
}
}
fn main() {
let logger = Some(Logger);
let data = "foo";
do_something(data, logger);
}
and rustc actually warns about this:
warning: for loop over an `Option`. This is more readably written as an `if let` statement
--> src/main.rs:8:14
|
8 | for l in logger {
| ^^^^^^
|
= note: `#[warn(for_loops_over_fallibles)]` on by default
help: to check pattern in a loop use `while let`
|
8 - for l in logger {
8 + while let Some(l) = logger {
|
help: consider using `if let` to clear intent
|
8 - for l in logger {
8 + if let Some(l) = logger {
|
I was actually going to say "I've never seen folks in Rust write out for loops this way, even though you can" and I suspect this lint has something to do with that!
I think this is most useful in generic contexts, I agree that I would not write this exact code this way because it does feel weird, if you know you have an Option, if let is the way to go. But if you're writing code over generic iterators, it can be nice that it exists.
Hah! I've internalized rustc and Clippy. Good job, team!
Yeah, I don't hate it. The semantics aren't wrong, either: an Option has 0 payload items or 1. It still feels wrong to me in ways I can't fully explain.
> the people writing the standard are not exactly known for adding features “just because”
Ah yes, C++, the discerning language.
Iterating over optional does seem syntactically convenient. My main question would be if it guarantees no overhead. For example, is there an additional conditional branch due to the iterator hiding the statically know fact that there's at most one iteration? I don't use C++ for its beauty, I use it for speed.
Note that despite this work in C++ you can't flip the script to if ("hello".contains(username)) unlike Rust
Rust will also let you: "FOO".contains(char::is_uppercase)
That's showing off three clever tricks Rust has that C++ does not, but one day it will just be compile time evaluated as true and that's one place C++ is richer today. Many other things in Rust can be but Rust's traits aren't today allowed to be compile time evaluated, so the fact that we could determine at compile time whether there's an uppercase letter in FOO isn't enough yet. One day.
Great to see they learned from Java, which initially made the mistake of not supporting the streams interface for their Optional type at first [1]. It was infuriating.
The example program seems confusing and pointless. Who uses the bell for debug logging? I found it distracted from the central point of the article as I stared at this bizarre program.
I do enjoy seeing C++ pick up these features from other languages.
... it's unfortunate that the feature then ends up in C++ syntax, which is increasingly divorced from the way it's used today, the kind of thing that nobody would have written from scratch if they were starting day-1 with these ideas in play. I look forward to having the features, but not to having to read things like `const auto flt = [&](int i) -> std::optional<int>` or `for (auto i : std::views::iota(1, 10) | std::views::transform(flt))`.
When we added the equivalent to Rust, it was a bit controversial. But the idea of "an optional type is a list with zero or one items in it" is much more normal in functional places, and so a lot of people like it quite a bit too.
The fun thing is that the original design (N3527 over a decade ago) for std::optional specifically says it's not the container with exactly zero or one items, whereas I believe Rust's Option never made this claim and so recognising this isn't a U-turn.
I really like how some good structures used by other languages, specially Rust and Zig, have been added to the newer C++ standard.
The Result, Optional, and Variant are really sweet for day-to-day use of the language, and those in-process standard libraries of SIMD operations, BLAS mathematical functions, and the the execution library looks really cool, specially as standard.
I would like C++ would be a little more "batteries included" in some ways, like having a basic standard for signals, networking (just handling sockets would be a huge thing), and some basic system calls.
> I really like how some good structures used by other languages, specially Rust and Zig, have been added to the newer C++ standard. The Result, Optional, and Variant are really sweet for day-to-day use of the language, and those in-process standard libraries of SIMD operations, BLAS mathematical functions, and the the execution library looks really cool, specially as standard.
for Optional and Variant they both were basically standardized versions of boost.optional & boost.variant, which exist since 2003 and 2002 respectively. Most of the time you can just change boost:: to std:: and it works exactly the same ; for many years software I develop could switch from one to another with a simple #ifdef due to platforms not supporting std::optional entirely (older macOS versions, pre 10.14 IIRC)
Correct, they have been around for a lot longer than rust.
"I would like C++ would be a little more "batteries included" in some ways, like having a basic standard for signals, networking (just handling sockets would be a huge thing), and some basic system calls."
Besides basic handling of TCP sockets and the Unix-style "Ctrl-c" keyboard interrupt, none of the stuff you're asking for is portable across different platforms. I'm not saying it's a bad idea, just that there is no one single universal standard for what an OS should do and what knobs and levers it should expose, or at least one that everybody follows.
Linux has non-trivial deviations from the POSIX spec, and even FreeBSD and OpenBSD have deviations. POSIX has its own compliance test suite that it runs to award certification of compliance, but it's not open source and it you need to pay a fee for it.
All of that however, is a drop in the bucket compared to making an API that exposes all the knobs and levers you want in a way that behaves exactly the same on Windows which barely has any architectural resemblance to UNIX. For exmaple, NTFS is case-insensitive by default and has nothing resembling the UNIX style of file permissions. Or more importantly, signals do not exist on Windows; something resembling signals for keyboard interrupts exists, but stuff like SIGHUP and SIGBUS does not. I'm talking the kind of known caveats that come with using a POSIX-compatibility layer on Windows, e.g. Cygwin.
I think if I get much deeper than that I'm just being pedantic, but even Python code behaves differently on Windows than it does on all the POSIX-like OSes out there.
> I would like C++ would be a little more "batteries included" in some ways, like having a basic standard for signals, networking (just handling sockets would be a huge thing), and some basic system calls.
Isn't Boost library basically that? C++ has been slowly adopting freatures from it to its standard library.
std::variant is an abomination that should never be used by anyone ever. Everything about it is sooooo bad.
That's a bit hyperbolic. Sure, it's not exactly ergonomic, but that doesn't mean I can't use it. One thing that bugs me is that there is still no overload helper in the standard:
Of course, having true pattern matching would be much nicer. At least there's a proposal: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p26...I will boldly state that std::variant makes all code worse and it is always better to not use it.
I love sum types in Rust. They’re great. This stuff exists solely to add ergonomics. If it’s not ergonomic it’s just making your code worse!
std::variant is fundamentally broken because it uses types as its discriminant. Want a variant with two ints that represent two things? Fuck you, you can’t. Rust enums of course can do this.
std::visit is an abomination. As is the template overloaded bullshit you have to copy paste into projects.
And of course the error messages when you get when it’s wrong are atrocious even by C++ standards.
No. std::variant is awful and you should simply never use it. This could change in 2040 when you can use C++32. But for now just avoid it.
>Want a variant with two ints that represent two things? Fuck you, you can’t.
This is false, I also find that in general you have a tendency to make false claims about C++ in most of your posts about it. I would suggest you check out a resource like https://en.cppreference.com/ just as a sanity check before you make claims about the language in the future, and also because it's a very good resource for learning the ins and outs of the language.
As for your claim, std::variant supports index based discrimination similar to std::tuple, so you can absolutely have a std::variant<int, int>, and access the second int along the lines of:
This is all documented with examples:https://en.cppreference.com/w/cpp/utility/variant/get.html
Would you ever really use that? That just shows how bad it is, IMO. Any time I want a sum type I want names, not hardcoded integers.
I treat std::variant the same way I treat std::tuple, which is that I use them internally/privately and don't expose them as part of a public API.
If I want to expose a std::variant publicly then I take the effort to emulate Rust, which I think everyone agrees has an incredibly useful and elegant enum type so it looks like this:
To implement that I currently do need to write out boilerplate like below, but with C++26 I will be able to use the upcoming reflection feature to automatically implement the bulk of this code by reflecting on the std::variant directly: ...> I will boldly state that std::variant makes all code worse and it is always better to not use it.
And I will boldly state that your comment is again completely hyperbolic.
> std::variant is fundamentally broken because it uses types as its discriminant.
In my experience, the typical use case for std::variant is static polymorphism. For this purpose it works just fine because the type is the discriminator.
Would proper sum types and pattern matching be nice? Of course! But this this mean that std::variant is fundamentally broken? I don't think so.
out of curiosity, can you elaborate on that a bit? I've shipped std::variant in a few designs and it's been OK, but maybe I didn't use it deeply enough to see issues.
It doesn't have proper pattern matching (you have to use a visitor class or the weird overload trick), and it sucks for compile times. It should have been a language construct rather than adding it to std, but the committee is often too scared to touch the language because they are stubbornly unwilling to break backwards compatibility.
C++ is a proud New Jersey language. So the priority has been and continues to be ease of implementation. If it's tricky to make it work right, don't sweat it - the users (in this case that's us programmers) will put up with it not working right.
std::variant has valueless_by_exception - the C++ language wasn't able to ensure that your variant of a Dog or a Cat is always, in fact, a Dog or a Cat, in some cases it's neither so... here's valueless_by_exception instead, sorry.
Ease of implementation was not the issue here, on the contrary the easier implementation would have guaranteed that std::variant<Dog, Cat> always contains a Dog or a Cat. The issue was performance in that guaranteeing a Dog or a Cat requires dynamic storage duration if either of Dog or Cat has a throwing constructor.
If neither Dog or Cat have throwing constructors, then std::variant<Dog, Cat> is guaranteed to always be a Dog or a Cat.
Herb Sutter calls this kind of thing "spelling things generically" meaning that the same generic code will compile against different types. In this case the same for loop code will compile against a type that may hold zero or one items and a type that may contain 0->n items. Maybe this pattern could be extended to shared_ptr for example.
It's sort of like a cleaner approach to a C# SelectMany where you return either an empty array or a single-item array?
C++ keeps getting more stuff but it's clear a lot of thought goes into these additions. I think a "living" language has proven better over time (C# evolved heavily and borrowed from other languages, C++ does as well, maybe lately concepts from functional and Rust?) Go might be the big exception that evolves super-slowly.
The for syntax over optional seems unnatural. I get that optional is like a list of 0 or 1 owns. Is there a fault in my reading that makes the syntax clunky?
Putting it through a for loop demonstrates that it is iterable, imo.
What seems weird here? Iterating or mapping over Options is pretty normal in functional languages.
Well, you can map over options/maybes. For-loop style iteration over an option does seem a little strange IMO, first, because syntactically it looks like overkill, and second (and more importantly), because map evals to a value, while `for` does not.
But I suppose in C++, given the centrality of looping constructs, this is what you would do to accommodate option.
With syntax questions, it can help to get very concrete. There's a few different bits of syntax in this post, what is it you find clunky?
Not the person you were replying to, but
bent my brain for a moment. `logger` is a list of loggers to send data do? Oh, no, it's either 0 or 1 loggers.Rust's `if let` syntax maps much more closely to how I think about it. I guess if this becomes idiomatic C++ then it'd start looking perfectly normal to everyone, but it still seems odd. For instance, I don't think I could ever bring myself to write Python like this, even if it worked:
That makes sense! What's funny is, I went to go write the equivalent Rust code:
and rustc actually warns about this: I was actually going to say "I've never seen folks in Rust write out for loops this way, even though you can" and I suspect this lint has something to do with that!I think this is most useful in generic contexts, I agree that I would not write this exact code this way because it does feel weird, if you know you have an Option, if let is the way to go. But if you're writing code over generic iterators, it can be nice that it exists.
Hah! I've internalized rustc and Clippy. Good job, team!
Yeah, I don't hate it. The semantics aren't wrong, either: an Option has 0 payload items or 1. It still feels wrong to me in ways I can't fully explain.
> I think this is most useful in generic contexts
And to build other iterators e.g. std::iter::once is a trivial wrapper around an Option::Some.
Seems like a good thing broadly. I haven't personally found a use for ranges, ever, but this seems consistent.
Maybe somewhere in C++60 we will finally have proper monads
> the people writing the standard are not exactly known for adding features “just because”
Ah yes, C++, the discerning language.
Iterating over optional does seem syntactically convenient. My main question would be if it guarantees no overhead. For example, is there an additional conditional branch due to the iterator hiding the statically know fact that there's at most one iteration? I don't use C++ for its beauty, I use it for speed.
> Ah yes, C++, the discerning language.
C++, the language that refused to add `contains()` to maps until, you know, C++20!
This gave me a good chuckle. For those wondering, `count` was the go-to and I can tolerate an argument that `contains` is a negligible improvement.
They didn't stop there, C++23 brought contains() to strings too! You can do some crazy stuff nowadays like say if(username.contains("hello"))
Absolutely incredible, definitely worth the wait
Note that despite this work in C++ you can't flip the script to if ("hello".contains(username)) unlike Rust
Rust will also let you: "FOO".contains(char::is_uppercase)
That's showing off three clever tricks Rust has that C++ does not, but one day it will just be compile time evaluated as true and that's one place C++ is richer today. Many other things in Rust can be but Rust's traits aren't today allowed to be compile time evaluated, so the fact that we could determine at compile time whether there's an uppercase letter in FOO isn't enough yet. One day.
Great to see they learned from Java, which initially made the mistake of not supporting the streams interface for their Optional type at first [1]. It was infuriating.
[1] https://stackoverflow.com/questions/22725537/using-java-8s-o...
The example program seems confusing and pointless. Who uses the bell for debug logging? I found it distracted from the central point of the article as I stared at this bizarre program.
This is absolutely magnificent. Game-changing improvement, really shows how amazing the C++ committees are working.
It's hard to tell if you are being serious or joking. And I say that as a C++ aficionado :)
I think the being an aficionado is clouding your judgement, because it's pretty obvious what the intent is.
Some people really are that enthusiastic about C++. Just watch certain cppcon talks. Poe's Law strikes again.
No it’s not.
I'm sensing a pinch of sarcasm
I do enjoy seeing C++ pick up these features from other languages.
... it's unfortunate that the feature then ends up in C++ syntax, which is increasingly divorced from the way it's used today, the kind of thing that nobody would have written from scratch if they were starting day-1 with these ideas in play. I look forward to having the features, but not to having to read things like `const auto flt = [&](int i) -> std::optional<int>` or `for (auto i : std::views::iota(1, 10) | std::views::transform(flt))`.