【问题标题】:Is it legal to have a pointer to a reserved vector element? [duplicate]有一个指向保留向量元素的指针是否合法? [复制]
【发布时间】:2015-02-19 07:35:53
【问题描述】:

我很好奇这种事情是否合法:

std::vector<some_class_type> vec;
vec.reserve(10);
some_class_type* ptr = vec.data() + 3; // that object doesn't exist yet

请注意,我并没有尝试访问指向的值。

这是标准对data() 的表述,但我不确定它是否相关:

返回:一个指针使得[data(),data() + size()) 是一个有效的 范围。对于非空向量,data() == &amp;front()

【问题讨论】:

  • 我会说指针的初始化本身是合法的。取消引用它是 UB。
  • 据我所知,您对reserve 所做的工作类似于malloc,其中分配了内存但未初始化。老实说,我对标准并不完全熟悉,但从逻辑上讲,如果这不合法,那么malloc 也不合法。
  • 我想这完全取决于数组内存是否必须在调用reserve 时分配,或者是否可以推迟到第一次访问。如果标准中没有明确的要求,我想某些实现可能会将分配推迟到 UB 可能最少。不确定对 data 的调用是否会被视为“首先评估”或是否足以调用分配。
  • @Nard:为了完整起见,也在这里回复——reservemalloc 之间的区别在于未来的调整大小操作(例如,由过去的向量增长触发它的容量)将导致数据被重新分配,可能使ptr指向的内存区域无效。
  • @EyasSH 换句话说,reserve 不一定调用mallocrealloc,而是由标准简单地定义为通知向量计划更改的函数size 所以它的实际作用是依赖于实现的,对吗?因此,将reserve 视为malloc 的谬误在于,在调用reserve 时可能不会分配内存,因此我想OP 所做的绝对是未定义的行为?

标签: c++ pointers vector language-lawyer undefined-behavior


【解决方案1】:
  1. data 必须返回一个指向有效范围的指针 ([vector.data])。指向该范围的指针,包括指向该范围最后一个元素之后的指针,在下一次重新分配之前必须保持稳定。当size() 为零时,data() 指向一个空范围的末尾,这是一个完全有效的要保留的指针(但当然不能取消引用)。
  2. 由于data 必须在接下来的 10 次插入中保持稳定,我们可以假设获得了一个大小和对齐方式适合放置 10 个元素的数组的存储区域,并且data 指向该区域的开头.也就是说,实现必须调用分配函数并将内部数据指针设置为其返回值。 (标准中没有直接表明这一定是真的,但我无法想象它可能是错误的情况。我假设标准要求的重新分配([vector.capacity])实际上调用了分配函数之一,例如@ 987654330@ 或 std::malloc,否则称此操作为“重新分配”将是相当可疑的。无论如何,在任何当前架构上似乎都无法避免这种分配。
  3. 任何这样的存储区域实际上都包含一个适当大小的实时数组 ([intro.object])(尽管不一定是实时数组元素,具体取决于元素类型)。这种数组中的指针算术是有效的,即使取消引用结果指针不一定有效。

【讨论】:

  • 我认为您在 2 中的假设在大多数情况下都是正确的。但是,我在标准中没有找到任何保证在将任何元素插入向量之前对空向量进行重新分配甚至分配的任何内容(请参阅我的引用,引用 C++11 或 C++14并且可能已经重新编号)。
  • 对于另一个似乎有类似疑问的问题还有另一个答案:stackoverflow.com/a/38972840/3723423
  • @Christophe 你是对的,空向量通常不存储。但是 reserve() 调用会强制向量立即分配内存,即使仍然是空的。
【解决方案2】:

在 STL 的大多数实现中,空向量的 reserve 将触发重新分配并确保指向它的数据是拥有/管理的。

当调整向量大小时,数据的位置(data() 返回的指针值)可能会改变。持有指针本身当然是合法的,在未初始化时取消引用它以进行读取当然是未定义的,并且在初始化后取消引用它只有如果你能保证是合法的你的向量没有调整大小,因此你分配的范围仍然在同一个地方。

增加指向 malloc'd 数据的指针是可以的。在此示例中,您执行指针运算来保存指向您知道已由std::vector 分配的数据的指针。无论指针指向的元素是否曾经被初始化,调整大小操作都是有问题的,因为它可能会释放您指向的内存。

【讨论】:

  • 我看不出指针本身会如何变化。但是,指针指向的数据可能会在 resize 操作期间发生变化。
  • @Nard 指针在容量变化时随时可能失效,因为底层实现可能会使用realloc,如果当前内存块无法就地扩展,它可以返回不同的地址。
  • 我很确定data() 返回的指针可以改变。 std::vector 保留移动数据块的权利。这就是向量如何在保持连续的同时增长其底层数组。
  • @Nard 用于data() 井的指针可能会因resize() 操作而改变。
  • @Nard - 数据的位置可以通过调整大小而改变。 data() 返回的指针是指向数据位置的指针。因此,data() 返回的值可以改变。
【解决方案3】:

总结

我们不能假设这个指针是有效的。此外,指针算法vec.data() + 3 可能是UB。而且由于没有任何东西可以保证它不是 UB,因此如果此代码有效,则它取决于实现。

注意:此答案已改写,以更好地区分 UB 和有可能成为 UB。

语言-律师推理

这个向量不是空的吗?

在您的代码 sn-p 中,您在 vec 的空向量 vec 上使用 reserve()data(),该向量的 size() 为 0。我们从您自己引用的标准中知道两件事:

  • 首先,data() 返回一个指针,使得[data(), data() + size()) 是一个有效范围。因此,在您的情况下,我们可以假设 [data(), data()+0) 是一个有效范围。但是[data(), data()+capacity()) 没有这样的保证。您的标准库可以提供这种依赖于实现的保证,但一般情况下您不能确定。如果不是,则表达式肯定是 UB(进一步解释)。
  • 其次,对于非空向量data() 返回第一个元素的地址。这是您做出的假设(否则您不会添加固定索引)。但由于你的向量是空的,你不能确定。例如,完全允许实现为空向量返回 nullptr,无论其容量如何。

为什么有效范围如此重要?

一个基本规则经常被忽略:如果指针算术运算不在有效数组对象的范围内,则它是 UB。该标准更正式地表达了这一点:

[expr.add]/4:当一个整数类型的表达式J与一个指针类型的表达式P相加或相减时,结果就是P的类型。

  • 如果 P 的计算结果为空指针值,而 J 的计算结果为 0,则结​​果为空指针值。
  • 否则,如果 P 指向具有 n 个元素的数组对象 x 的数组元素 i,则表达式 P + J 和 J + P(其中 J 的值为 j)指向(可能是假设的)数组元素 i + j of x if 0 ≤ i + j ≤ n 并且表达式 P - J 指向(可能是假设的)数组元素 i - j of x if 0 ≤ i - j ≤ n。
  • 否则,行为未定义。

这意味着如果将整数添加到指针,结果将超出有效范围,则表达式本身将是 UB,甚至在计算指针结果之前。这也意味着将 0 以外的任何内容添加到 nullptr 也将是 UB。

所以如果你的库的向量实现严格遵守标准,没有任何额外的保证,你的代码将是 UB,因为这个指针算术规则(超出范围)。但由于我们不知道您的实现是做什么的,我们无法确定 UB。我们唯一确定的是不能排除UB,代码不可移植。

其他想法

可能很容易相信reserve() 保证了正在分配的向量的内存,因此保证了范围[data(), data() +capacity()) 的有效性。但事实并非如此:指针算术规则不是关于分配的内存,而是关于 具有 n 个元素的数组对象的数组元素

一个实现可以很好地分配内存并使用placement new 创建正确大小的数组对象,以保留现有元素的地址。这不会是一个超级有效的实现,但它会是一个合法的。

此外,该标准为reserve()capacity() 提供了关于不存在重新分配的保证:

[vector.capacity]/4:一个指令,通知向量计划的大小变化,以便它可以相应地管理存储分配。在reserve() 之后,如果发生重新分配,则capacity() 大于或等于reserve 的参数;否则等于 capacity() 的前一个值。 重新分配此时发生当且仅当当前容量小于reserve()的参数。

[vector.capacity]/1:返回:向量无需重新分配可以容纳的元素总数。

但只要向量保持为空,就不会重新分配任何元素。因此,符合标准的实现不必担心任何重新分配,并且可以在第一个元素插入向量之前及时延迟第一个分配。我个人不会那样执行它,但它是合法的,不能被排除在外。当向量为空时,data() 没有义务返回指向第一个元素的指针,这一事实似乎是为允许这种实现而量身定制的。

最后一句话

您的代码将适用于主流实现,因为出于实际原因,reserve() 触发分配/重新分配是很常见的。但是,如果您想要可移植的代码,它也可以完美地在任务关键型系统中的奇异微控制器架构上运行,并且有生命危险,那么您最好避免使用这种捷径。

【讨论】:

  • 我不会对空向量执行此操作。此外,该措辞保证调用保留后内存存在。
  • @Pubby:在您发布的代码中,您正在对一个空向量执行此操作。
  • 我没有否决它,但我假设它是因为“当且仅当”语言似乎暗示保留将保证您在之后分配的内存至少为该大小调用它(因为如果你不这样做,那是因为你的容量更大),并且他们假设容量 > 0 保证存在相应的 data 区域。
  • 备注:重新分配使所有引用序列中元素的引用、指针和迭代器失效。 在调用 reserve() 之后发生的插入过程中不会发生重新分配,直到插入会使向量的大小大于 capacity() 的值 -- 如果我调用 @ 987654339@ 那么懒分配是不可能的...
  • 该标准使用术语“重新分配”来表示“初始分配”和“(真实)重新分配”。这从以下句子中可以清楚地看出:“当且仅当当前容量小于reserve() 的参数时,才会在此时发生重新分配。”如果容量为零,则没有发生分配,重新分配成为初始分配。
【解决方案4】:

在对 vector::reserve() 的注释中,C++17 标准规定:“在调用 reserve() 之后发生的插入过程中,直到插入的大小达到大于 capacity() 值的向量。"

在对vector::shrink_to_fit()的注释中,相同的标准规定:“重新分配使所有引用序列中元素的引用、指针和迭代器以及过去的迭代器无效。如果没有发生重新分配,它们仍然有效。”

结合这两个导致这样的声明:在调用reserve() 之后,任何插入都不能使指针无效,只要最初保留的容量得到尊重。由于data() 返回的值显然是一个指针,规则适用于它。因此,如果应用程序使用正数调用reserve(),则实现必须立即设置其data() 指针。只有在可能发生重新分配时才会改变它。

有些人可能会说,标准谈论“指向单个元素的指针”,不会失效,但data() 不指向任何元素,如果向量有保留空间,但仍然是空的.但是,标准说:“指针......指的是序列中的元素”。 data() 显然是指 all 序列中的元素。从数学上讲,一个空集仍然是一个集合,一个空序列仍然是一个序列,指向空序列的data() 指针在以后放大该序列时必须不会失效(当然,除非它的@ 987654329@已用尽)。

但是指向空白空间的指针算术呢? vec.data() + 3?好吧,在 C 中,这显然是一个有效的操作,正如我们所知,vec.data() 指向 10 个元素的空间,因此将三个元素推进到其中就可以了。在 C++ 中,这种类似 C 的指针算术仍然是合法的,只要我们不敢在这些指针变为有效之前取消引用它们。

【讨论】:

  • 关于 data() 的假设显然是错误的。它返回不应失效的指针这一事实仅适用于非空向量。 data() 的规范是这样制定的,如果向量为空,则没有义务返回指向第一个元素的指针,因此这里没有重新分配的情况。
  • 这里有一个遇到问题的人的案例:stackoverflow.com/q/38972443/3723423
  • 感谢有问题的人的链接。不幸的是,他没有提到使用的编译器和库,也没有提到他的代码。所以我们不知道,如果他的代码因为这个特殊问题或其他原因而崩溃!特别是,那家伙说:“既然迭代数据就会崩溃......”他甚至没有说他插入了足够的数据来迭代它!
  • 请指出标准的vector 描述中的哪个位置,它说:“它返回一个不应失效的指针的事实仅适用于非空向量。”相反,标准非常明确:“如果没有发生重新分配,它们 [所有迭代器、指针、引用] 仍然有效。”等式 data() == addressof(front()) 在标准中仅限于非空向量,因为 front() 是对空容器的非法操作。
  • Section [vector.data] (aka 22.3.11.4 for C++20) 说“返回一个指针,使得 [data(), data()+size()] + 是一个有效范围. 对于非空向量 data()==addressof(front)"。最后一句话是这里的关键:对于一个空向量,根本无法保证数据指向第一个元素。任何其他说法都是纯粹的解释。幸运的是,它在主流实现上可能如您所想的那样工作,但您不确定这在外来实现上是否可移植。
【解决方案5】:

当然,这是合法的。 您提到的报价无关紧要,因为size 不等于reserve 提供的“保留空间”。 你也可以在vec[0]之前初始化vec.data()+3,虽然向量的“size”变量不会被更新。

因此,虽然向量的这种使用是非常不可取的,但向量只不过是动态分配数组的瘦包装器,以这种方式滥用向量并不违法。

根据经验:一旦你使用了vector::data() 函数,你就做错了。

【讨论】:

  • 我强烈建议您看一下:stackoverflow.com/q/10473573/3723423指针算法如果不在正确分配的数组的范围内,则为 UB,即使您不使用指针)。因此,OP 的问题引用在这里是完全相关的,因为 data() 仅保证底层动态管理的数组具有 size() 的上限,并且 OP 执行超出该大小的算术。
  • 请再次阅读 OP 和我的帖子。 OP 的引用是无关紧要的,因为他的 reserve(10) 语句就在 data()+3 语句之前。
  • 当然,理论上,您总是可以编写一个不为任何 n 保留任何内容的分配器(这不符合标准)。那么,在这种退化的情况下,这是非法的操作。但这不是 OP 或我的观点。
  • 感谢您的反馈。我担心问题不在于它是否普遍有效(以及是否是“退化”案例),而在于是否符合标准。它要么是 100% 兼容的,要么是有风险的不可移植代码。每周都有关于网络攻击和 0 天漏洞利用的报告。假设这段代码在 98% 的情况下都有效,但如果它是 UB,而 2% 发生在嵌入飞机自动驾驶仪或医疗通风系统的基本微控制器上,可能会危及生命?
  • 嗯,这就是我要说的:它是合规的并且是合法的。而且我已经写过我不会推荐它,尽管我没有你现在那么戏剧化。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-12
  • 1970-01-01
  • 1970-01-01
  • 2014-10-10
  • 2022-01-12
  • 2019-12-28
相关资源
最近更新 更多