Granted i have learnt better and should probably create an enum that explicitly describe what all the different states represent.
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.
C++, the language that refused to add `contains()` to maps until, you know, C++20!
Absolutely incredible, definitely worth the wait
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.
namespace std {
class string {
bool contains(const string& other) & {
//Check if 'this' is in 'other'
}
bool contains(const string& other) && {
//check if 'other' is in 'this'
}
};
}
using namespace std::string_literals;
int main() {
string foo("foo bar foo");
foo.contains("bar"); //returns true
"bar"s.contains("foo bar foo"); //returns true
}
But I hope not because this 'flipping the script' behavior just makes the code flow more difficult to reasonate about.Notice how you needed to write "bar"s to make your code compile? That trailing 's' says you want specifically to construct a std::string not use the language's built-in string literals. So you'll need a full blown hosted C++ environment (otherwise there's no allocator for the string class)
The Rust string literal was inherently a &'static str, which only needs the core language, it Just Works™.
There's a weird asymmetry, Bjarne wants user defined types to have all the features the built-in types have, but, not vice versa. So you can overload the short circuiting boolean operators on your own types (don't, this is basically always a bad idea) but you can't call methods on built-in literals like "foo" or true or 'X'...
https://en.cppreference.com/w/cpp/string/basic_string_view/c...
With the string literal suffixes, I don't think it's all that different ("hello"sv.contains(username)). Yes, yes, for legacy reasons bare "" is the C-style string, but you can get a string_view string easily enough.
Exactly like the article topic. Iterating over your maybe type was IMO an obvious thing you'd want, but it took C++ a decade to provide this behaviour in a slightly awkward way.
And likewise for their maybe reference, which C++ 26 also finally lands. The original discussion papers for std::optional explain the core ideas for std::optional<T&> but apparently the possibility that it's an assign through type, a thing nobody has ever wanted, and no other language has ever provided - was so important that it blocked all work on this idea for about a decade.
The main difference there seems to be that GCC treats the if() as unlikely to be taken while the for() as likely.
for (auto l : logger) {
l.log(data);
}
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) 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.
And to build other iterators e.g. std::iter::once is a trivial wrapper around an Option::Some.
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’m not asking to dispute. I want to be enlightened!
But I suppose in C++, given the centrality of looping constructs, this is what you would do to accommodate option.
Trying to gaslight ppl to question their reasonable reaction of std::optional transitioning from its previous behavior to an Option like behavior when no other type in C++ behaves as such is disingenuous.
Of course it is weird in the C++ context, but is it a step in the right direction? absolutely!
template <typename T>
void foo(const T& container) {
for (auto value : container) {
// process value
}
}
foo(std::vector<int>())
foo(std::optional<int>()) if (x.has_value()) {
doSomething(*x);
} auto it = std::ranges::find(ctx.users, req_user_id, &User::user_id);
If ctx.users is a vector<Users>, it will return an iterator pointing to the first matching User, by matching a field in User to a variable.It's just syntax sugar for std::find and a simple lambda but it's been really nice syntax sugar.
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.
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)
That means I need to look more Boost documentation :)
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.
If neither Dog or Cat have throwing constructors, then std::variant<Dog, Cat> is guaranteed to always be a Dog or a Cat.
Any need here for an allocator is due to a language defect. Rather than implement a fix for the C++ programming language they just punted this problem to every C++ programmer. That's the New Jersey style.
// helper type for the visitor
template<class... Ts>
struct overloads : Ts... { using Ts::operator()...; };
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 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.
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:
auto foo = std::variant<int, int>();
std::get<1>(foo) = 123;
std::cout << std::get<1>(foo);
This is all documented with examples: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: class ConnectionState : private std::variant<std::monostate, int, IpAddress> {
public:
static auto Disconnected() { return ConnectionState(std::monostate{}); }
static auto Connecting(int retries) { return ConnectionState(retries); }
static auto Connected(std::string host, uint16_t port) {
return ConnectionState(IpAddress{std::move(host), port});
}
template <typename... Fs>
decltype(auto) visit(Fs&&... fs) const {
auto visitor = Overload{std::forward<Fs>(fs)...};
return std::visit(visitor, *this);
}
private:
using std::variant<std::monostate, int, IpAddress>::variant;
};
using Disconnected = std::monostate;
...If anyone ever submitted a diff that required you to know the difference between get<1> and get<2> I would reject it with a polite message that this is extremely unclear, unintuitive, and error prone. Don’t do that.
I write C++ every day and use cppreference on the regular. If you’re going to scroll my post history you can also read my blog to help decide if I’m a dumbass or not!
Also, if you create a variant with multiple instances of the same type you now lose the ability to use visitor overloaded lambdas. So in practice you need to wrap the type. Which in some ways is what Rust does. But all that is to say that std::variant is extremely non-ergonomic and you’re better off just not using it.
But that wasn't your argument. If you have a coding standard that prohibits magic numbers then that's great, use a named constant instead just like most coding standards require:
constexpr auto FOO = 1;
constexpr auto BAR = 2;
std::get<FOO>(my_variant);
std::get<BAR>(my_variant) = "hello world";
>If you’re going to scroll my post history you can also read my blog to help decide if I’m a dumbass or not!I don't think you're a dumbass, I think you repeatedly express very strong opinions without taking just a small amount of time to verify that the argument you're making is correct. That's why I advised to just take like 1 or 2 minutes to quickly perform a sanity check and ensure that what you're claiming is factual.
Heck most people here complain about C++ constantly based on their personal experience with the language, and they have every right to do so. I don't take issue with that.
I take issue when people express very strong statements that would convince people who don't know any better simply on the basis of how confident the opinion is being expressed. Your original claim is simply too strong of a claim to make given that you are not properly informed on this subject.
Named constants aren’t significantly better. Multiple types in a variant breaks many things with god awful error messages. And that is a fact.
I am hyperbolic on HN. That’s true. My sentiment is sometimes but rarely wrong!
std::variant is bad and no one should use it ever, imho. It sucks and is horribly ergonomic and doing certain things makes it even less ergonomic. Friends don’t let friends use std::variant.
Now ask me my opinion on global variables and how many times I have had to debug mysterious crashes that you’ll never guess the root cause!
Are you sure this isn't projection?
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.
Another popular use case is tagged unions for non-POD types. In this case, you probably access the active member with std::get. This even works with duplicate types, if you access by index.
Would proper sum types and pattern matching be nice? Of course! But does this mean that std::variant is fundamentally broken? I don't think so.
Now purists will scream “compile time optimization!”, but in reality std::variant is inplemeted very múch líne a vtable. There was a good talk at Cppcon a few years ago on this issue. In particular, they found no difference in performance.
They're different tools which can fit different situations better or worse. std::variant fits some situations better (e.g., expressing a known closed set of types which may or may not share a common interface - tree nodes, option/result types, state machines, etc.), while interfaces/virtual functions fit some other situations better (e.g., expressing a potentially-open set of types with a shared interface - widget hierarchies, type erasure, etc.).
> Now purists will scream “compile time optimization!”
I actually feel like under most circumstances it's how closely one option or the other matches your problem domain that influences the decision one way or the other, not performance.
In any case, it doesn't really help that std::variant is somewhat neutered in C++ by a rather awkward interface as well as the lack of generalized/ergonomic pattern matching such as that offered by Haskell/ML-likes/Rust.
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.
Thanks for the long comment!
However, Windows NT was also written after machines capable of running Unix cost less than a BMW so a lot of the good folks in Redmond during the early 90s took some liberties to improve on some fundamental design flaws of UNIX.
1. "Everything is a file" is very flexible for writing server applications where the user is expected to know and trust every program, but it is potentially harmful to expose devices as files to non-technical users. Nowadays with UEFI, you can even pipe /dev/zero to /dev/mem or /dev/port and brick your motherboard. There was a patch for this, but there are old servers running old Linux versions in the wild that can be permanently bricked.
2. Arguably, exposing such a wide range of signals to a userland program for it to handle is a design flaw, like the memory fault signals SIGSEGV and SIGBUS. They were not designed for IPC or exception handling, but they ended up being used that way by a lot of developers over the years. I won't start a war to make the case because I can see both sides on that, but #3 below is not controversial at all.
3. NTFS ACLs are a big improvement over UNIX-style ugo-rwx permissions. FWIW, they're also easier to work with than POSIX ACLs.
Just something to think about: the Windows way is radically different because compatibility with ye-olde DOS running on 68k CPUs ruined it in some ways, but in other ways its design was driven by learning from UNIX's mistakes.
despite the confusing name, Win32 is not just a 32-bit libc, it's a 64-bit libc on 64-bit Windows.As much as possible in the stdlib should behave the same across as many targets as possible. That's about where the relevance ends in my mind.
Isn't Boost library basically that? C++ has been slowly adopting freatures from it to its standard library.
Boost in particular is like a testing ground for future C++ standards, with many of the "batteries" you want included. And it is already C++.
Of course, Rust is a huge influence nowadays, and it sparks a lot of debates on the direction C++ should take. I think less so with Zig, which is more C than C++ in spirit, but every good idea is good to take.
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.
... 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))`.
criemen•3mo ago
[1] https://stackoverflow.com/questions/22725537/using-java-8s-o...