When and why should I use std::monostate instead of std::optional with an std::variant?
Asked Answered
A

3

5

Let's say I am writing a class that passes a std::variant<A,B> to another class.

While I design the class, I decide I'd like to keep a copy of the last thing that was passed to that other class. Since on startup nothing will have been passed yet, it makes sense to me to store it as a std::optional<std::variant<A,B>>.

But std::monostate exists, and I'm under the impression that it was created to be used specifically in this kind of situation (here with std::variant<std::monostate,A,B>)

The thing is, I can't see why I should use it:

  • it seems to me to be less expressive than using std::optional
  • I don't want to pass the other function a std::variant<std::monostate,A,B> (otherwise it'll have to handle the case in which it contains nothing), but as far as I can tell, I cannot assign a std::variant<A,B> object to a std::variant<std::monostate,A,B> variable, so I'd have to manually handle the conversion

So, if not here, when should I use std::monostate instead of std::optional with std::variantand why? Or have I missed something that would make it a good choice here?

Audwen answered 5/11 at 14:31 Comment(3)
Adding std::monostate to your variant never increases its size. Putting the variant in std::optional always increases its size.Matsu
It's not the primary use-case, but you can use it as an optimisation. The prime use of std::monostate is to make std::variant default constructible in case no alternative were.Girlfriend
Some discussion recently by Raymond Chen: devblogs.microsoft.com/oldnewthing/?p=109959Ibex
K
6

Personally i use std::monostate when storing a variant that could be empty, using an std::optional is an extra 8 bytes, so if you have many of this variant then those bytes add up.

One example i have is a DragLogic which could be empty when no dragging is possible, it is a variant of multiple drag modes, or std::monostate if no drag is possible.

As for std::optional<std::variant<>> this could be an argument to a function or a return type, but storing optionals is wasteful, an object that consists of 8 optionals is wasting up to 56 bytes of padding because each bool is taking up 8 bytes instead of 1 on a 64 bit system, this waste is not needed for std::variant which can represent an empty state, the same thing can be said about pointers which could be null.

Kor answered 5/11 at 14:45 Comment(6)
Correct me if I'm wrong, but the size of a std::optional is that of a pointer, while the size of a std::variant is the size of the largest type it can hold, right? If so, does that not mean that for any type larger than a pointer it takes less space to use std::optional?Audwen
@Audwen The size of a std::optional<T> is the size of T, plus the size of bool, plus any applicable padding.Matsu
is that of a pointer no, it's the size of contained type + bool indicating engaged or not. Also keep padding in mind.Spectrum
Ah, so the 64 bits are not the size of a pointer but the size of padding! I get it now =)Audwen
@Audwen correct. The size of a std::optional<char> would be 2. "the size of a std::variant is the size of the largest type it can hold, right?" No. It's the size of the largest type, plus the size of a std::size_t indexing its true type, plus any padding. The size of a std::variant<char> might be 1 (char) plus 8 (size_t) plus 7 (padding).Matsu
@DrewDormann fyi, the size of std::optional<T> is not determined by anything esle than T. It is "atleast sizeof T", but how the "is engaged or not" is stored is not in the standard. It could be stored in a padding byte of the original T, it could be a boolean, it could be some magical bit within optional itself or a simple int. So saying "size of T plus size of bool" is wrong.Humanly
S
3

As for monostate (slightly off-topic): cppereference explicitly states, that it is meant to be used for non-default constructible variant where default construction is needed.

Consider the following code:

struct S { explicit S(int){} };
struct Z { explicit Z(char){} };
S getS();
Z getZ();
// (...)
void f()
{
  std::variant<std::monostate, S, Z> vsz;
  if (//whatever) {
      vsz = getS();
  }
  else {
      vsz = getZ();
  }
}

In this piece of code, if there were no monostate, it wouldn't compile due to the fact, that variant initializes its leftmost type by default. Sure, this particular piece of code could be rewritten to a form where it is not an issue. But other ones possibly could not.

Also, std::optional<std::variant<S, Z>> would certainly work here as well. However, optional carries extra overhead of the boolean flag. If it matters for your particular case is an open question. monostate however, does not have that overhead.

API-wise, it might be slightly more difficult to dereference an empty variant. Sure, one can always do *get_if<X>(&v); instead of visitation or throwing API but it's still easier to spot than plain *x.

Can this be also achieved using optional? Partially. With C++23 monadic interface, to some extent, but nothing (coding standard aside) prevents the programmer from just calling the * operator on it. It is possible though using some replacements, like Sy Brand's tl::optional.

To sum up, what should be used is a design decision; it can be taken based on ones taste (I noticed that my colleagues who use C++ interchangably with Rust tend to lean towards variants and visitation instead of switch-case, optionals etc., but that's just my personal observation, YMMV), company-established coding guidelines, library availability (maybe external dependencies are not welcome?) and various other factors.

Spectrum answered 5/11 at 15:5 Comment(2)
std::optional<std::variant<S, Z>> vsz; would also work... OP wants comparison with std::optional, not variant with or without std::monostateHeriberto
@Heriberto I rephrased my answer, thanks.Spectrum
A
1

Besides of the already mentioned differences from the other answers (storage size and whether to need one or two container classes for usability), there seems to be no important reason why you should or shouldn't use std::optional<std::variant<A, B>>.

From my personal point of view I would use the two types for different semantical use cases, there can be a difference whether you have an empty value or no value at all.

For example when you have a method to overwrite several members of a class:

  • a std::variant<std::monostate, A, B> parameter could mean that you will overwrite the prior content in any case, but the new content can be empty
  • a std::optional<std::variant<A, B>> parameter could mean that you can choose whether to overwrite the prior value or not, but if you wish to overwrite then the value type can only be A or B
Airflow answered 7/11 at 17:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.