假设我正在编写一个将传递std::variant<A,B>给另一个类的类。

在设计类时,我决定保留传递给另一个类的最后一个内容的副本。由于启动时尚未传递任何内容,因此将其存储为 对我来说是有意义的std::optional<std::variant<A,B>>

std::monostate确实存在,而且我的印象是,它是专门为在这种情况下使用而创建的(此处用std::variant<std::monostate,A,B>

问题是,我不明白为什么我应该使用它:

  • 在我看来,它不如使用std::optional
  • 我不想传递另一个函数 a std::variant<std::monostate,A,B>(否则它必须处理不包含任何内容的情况),但据我所知,我无法将对象分配std::variant<A,B>std::variant<std::monostate,A,B>变量,所以我必须手动处理转换

那么,如果这里不适用,那么我什么时候应该使用std::monostate而不是使用std::optionalwith std::variant,为什么?或者我是否错过了一些可以让它在这里成为好选择的东西?

2

  • 4
    添加std::monostate变体不会增加其大小。放入变体std::optional总会增加其大小。


    – 

  • 1
    这不是主要用例,但您可以将其用作优化。其主要用途是在没有其他选择的情况下std::monostate使默认可构造。std::variant


    – 


最佳答案
2

std::monostate我个人在存储可能为空的变体时使用,使用std::optional额外的 8 个字节,因此如果您有许多这种变体,那么这些字节就会加起来。

我有一个例子是,DragLogic当无法拖动时,它可能为空,它是多种拖动模式的变体,或者std::monostate如果无法拖动。

至于std::optional<std::variant<>>这可以是函数的参数或返回类型,但存储可选项是浪费的,由 8 个可选项组成的对象浪费了多达 56 个字节的填充,因为每个 bool 在 64 位系统上占用 8 个字节而不是 1 个字节,对于std::variant可以表示空状态的指针来说,这种浪费是不必要的,对于可以为空的指针也可以这样说。

6

  • 如果我错了请纠正我,但是 a 的大小std::optional是指针的大小,而 a 的大小std::variant是它可以容纳的最大类型的大小,对吗? 如果是这样,那是否意味着对于任何大于指针的类型,使用所需的空间都更少std::optional


    – 

  • 3
    @Eternal a 的大小std::optional<T>是 的大小T加上 的大小bool再加上任何适用的填充。


    – 

  • 1
    is that of a pointer不,它是包含的类型的大小 + 表示是否已接合的 bool。还请记住填充。


    – 


  • 1
    @Eternal 正确。a 的大小std::optional<char>应该是2“std::variant 的大小是它可以容纳的最大类型的大小,对吗?” 不。它是最大类型的大小,加上索引其真实类型的大小std::size_t,再加上任何填充。a 的大小std::variant<char>可能是1(char) 加上8(size_t) 加上7(填充)。


    – 


  • 1
    @DrewDormann 仅供参考,的大小std::optional<T>不由 T 以外的任何因素决定。它“至少是 T 的大小”,但“是否参与”的存储方式不在标准范围内。它可以存储在原始 T 的填充字节中,可以是布尔值,可以是可选本身中的某个神奇位或简单的 int。因此说“T 的大小加上 bool 的大小”是错误的。


    – 


至于monostate(稍微离题):明确指出,它旨在用于需要默认构造的非默认可构造变体。

考虑以下代码:

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();
  }
}

在这段代码中,如果没有monostate,它将无法编译,因为该变体默认初始化其最左边的类型。当然,这段代码可以重写为不存在问题的形式。但其他代码可能不行。

此外,std::optional<std::variant<S, Z>>这里当然也可以。但是,可选的布尔标志会带来额外的开销。这是否对您的特定情况很重要是一个悬而未决的问题。
monostate但是,没有这种开销。

从 API 角度来看,取消引用空变量可能稍微困难一些。当然,人们总是可以这样做,*get_if<X>(&v);而不是访问或抛出 API,但它仍然比普通的更容易发现*x

也可以使用 来实现吗optional?部分可以。在一定程度上,使用 C++23 单子接口,但没有什么(除了编码标准)可以阻止程序员在其上调用 * 运算符。不过,使用一些替代品是可能的,比如 Sy Brand 的tl::optional

总而言之,应该使用什么是设计决策;它可以根据个人喜好(我注意到我的同事在交替使用 C++ 和 Rust 时倾向于使用变体和访问而不是 switch-case、可选项等,但这只是我个人的观察,YMMV)、公司建立的编码指南、库可用性(也许不欢迎外部依赖?)和其他各种因素来制定。

2

  • 1
    std::optional<std::variant<S, Z>> vsz;也可以… OP 想要比较std::optional,而不是有或没有的变体std::monostate


    – 

  • @Jarod42 我重新措辞了我的答案,谢谢。


    –