// 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.
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.