为什么 std::optional 不使用 sentinel 值来表示空的可选?

Why does std::optional not use a sentinel value to represent an empty optional?

提问人:NoSenseEtAl 提问时间:10/15/2023 最后编辑:EvgNoSenseEtAl 更新时间:10/16/2023 访问量:167

问:

我知道这艘船由于需要的 ABI 破损而航行,但我想知道为什么最初实现没有决定使用一些魔术位模式来 , , 等......表示空可选值。这显然需要在类型的位表示中有一些“自由”值,因此例如它不适用于 ,但许多复杂类型具有某些表示形式,这些表示形式在有效实例中永远不会发生。std::stringstd::vectorintsize_t

C++甚至可以指定某种方式来选择你的类型,例如通过specialing

std::nullopt_sentinel

用于用户定义的类型。

我最好的猜测是,指定/实现这一点的复杂性(因为您需要为许多类型实现哨兵)比始终使用布尔值要大得多,但我想知道是否还有其他原因。std::

Rust 的类似答案似乎有时会实现这一点

https://stackoverflow.com/a/16515488/700825

C++ 语言设计 标准可选

评论

0赞 bolov 10/15/2023
“免费”未使用的值/位模式会是什么?std::vectorstd::string
1赞 Nicol Bolas 10/15/2023
"例如,它不适用于 int,size_t“ 你似乎已经回答了自己的问题。
2赞 Jan Schultke 10/15/2023
@Red.Wave不,它不需要额外的测试。检查只会检查存储的内容是否处于无效状态。您只需将一个测试替换为另一个测试即可。 对于这种状态,仍然是 UB,我看不出实现细节的更改如何以某种方式导致用户端的“不良做法”。严格别名似乎无关紧要,因为您没有对任何内容进行别名。这种专用化将简单地存储为数据成员。std::optional::empty()std::stringoperator*std::optionalstd::optionalstd::string
1赞 Drew Dormann 10/19/2023
@NoSenseEtAl 这个想法可能在成为论文之前就已经死了。这里有一些关于它的讨论。我不想在答复中引用这一讨论,因为它密切涉及一个被认为对社区非常有害的关于妇女和儿童的名字。欢迎您或其他任何人在发布的答案中引用该链接。
1赞 NoSenseEtAl 10/21/2023
@DrewDormann感谢您的链接

答:

4赞 user17732522 10/15/2023 #1

标准库和任何专门针对自己类型的用户已经可以自由地以这种方式实现它(参见 [namespace.std] p2)。std::optional

在强制执行的规范中,没有任何内容使用标志来指示值的存在。如何识别空状态是一个实现细节。如果可选为空,则用户不能依赖于观察有关内部状态的任何其他内容。std::optionalboolstd::optional

评论

0赞 HolyBlackCat 10/15/2023
用户甚至被允许专化吗?如果标准向其添加更多成员,这可能会很脆弱。optional
0赞 Nicol Bolas 10/15/2023
@HolyBlackCat:如果专用类型不在命名空间中,他们可以对其进行专用化。std
1赞 user17732522 10/15/2023
@HolyBlackCat 允许用户对依赖于用户定义类型的类型的所有标准库模板进行专用化,只要用户专用化也满足对标准库实现施加的要求,并且假设没有特定的例外,但事实并非如此。(见 eel.is/c++draft/namespace.std#2std::optional)
0赞 user17732522 10/15/2023
@HolyBlackCat 如果新标准扩展了接口,那么,就像标准库实现本身一样,用户必须为其类型实现新接口。标准库和其他用户可能会假设每个专业化都根据使用的 C++ 版本完全实现接口。
0赞 user17732522 10/15/2023
@HolyBlackCat 但这种情况并不少见。、 等通常是专用的,您可能会在新的 C++ 修订版中遇到同样的问题。std::numeric_limitsstd::hash
0赞 Nicol Bolas 10/15/2023 #2

假设一个类型可以识别它具有表示“不是值”的“魔术位模式”。比方说,根据是否宣传这一点,将以不同的方式实现。optional<T>T

那么这里有一个问题:这个“魔术位模式”实际上是一个有效的类型值吗?T

如果这个问题的答案是“是”,那么我们就有问题了。因为它是 type 的有效值,所以用户可以创建具有该值的类型的对象,对吧?所以。。。如果他们这样做会怎样?被认为是“已参与,但包含价值”还是被认为是未参与?TToptional<T> t(invalid_value);ttoptional

如果是前者,那么如何辨别和的区别呢?这两个 s 的位模式是相同的,因此它们的行为必须相同,对吧?optional<T>optional<T> t{}optional<T> t(invalid_value);t

所以如果是后者,那么你告诉我这个代码:

T t = something();
optional<T> ot(t);
if(ot)
{
  //Do something with ot
}

你有没有可能永远不会根据什么回报做某事?这是荒谬的。从代码的结构来看,你给它一个 ,如果你给它一个,它应该存储一个。这就是该类型的用途。otsomethingtoptional<T>T

所以,回到问题,现在必须认为这个“魔术位模式”不是 type 的有效值。也就是说,您无法创建具有此“魔术位模式”的模式。TT

现在,您如何实现这一点?当你构造一个未参与的,它必须将存储视为只是一袋比特。它必须使用从某个基于接口获取的值来初始化这组位。然后,为了测试是否接合,它必须将所有这些位与未接合的位进行比较。optional<T>TToptional<T>

现在,这要求它不能轻易复制。或者微不足道地默认可构造。否则,可能会创建具有无效值的 a。TT

但更重要的是,你甚至错过了想要这个的关键原因之一:空间效率。

看,大多数“许多复杂类型[它们]具有某些表示形式,而这些表示形式永远不会出现在有效实例中”,这些类型都比 .即使是 a 的大小也是 a 的 3 倍(包括 64 位指针的填充),更不用说小字符串优化了。由于这些类型中的大多数都比用于表示它们的跟踪数据大得多,因此空间效率的论点并不是特别令人信服。boolvector<int>boolstd::string

大多数情况下,需要这种空间效率的情况是不可为 null 的指针或枚举器之类的东西。也就是说,您希望 null 指针值像未参与的 ,并且您希望不是枚举器之一的枚举值像未参与的 。这些是小类型,与 的大小相当,因此当前的实现会将其大小增加一倍。节省空间非常大。optional<T*>optional<E>booloptional

越大,节省空间的意义就越小。T

但你会注意到另一件事。越大,测试是否未接合所需的时间就越长。如果大小为 24 字节,则必须比较 3 个 8 字节的字以查看它是否参与。Toptional<T>T

同样,只有在很小的情况下才有意义。越大,情况越糟。TT