【问题标题】:How risky is it to destroy_at/construct non-copyable vector elements?破坏/构造不可复制的向量元素有多大风险?
【发布时间】:2021-10-28 15:33:22
【问题描述】:

我已经通过 SO 进行了搜索,并且对以下内容是否安全有疑问。

考虑这个向量:

std::vector<std::pair<const key, value>> vec;

想象一下,我想将i 位置的元素与最后一个元素交换并弹出。 暂时忽略这样一个事实,即如果出现问题,向量可能处于不安全状态。我完全知道这一点,但问题更多的是关于周围的其他操作。 由于我不能交换这两个元素,我很想这样做:

auto *elem = std::addressof(vec[i]);
std::destroy_at(elem);
std::construct(alloc, elem, std::move(vec.back());
vec.pop_back();

同样,我们在第 2 行和第 3 行之间处于不安全状态,但让我们忽略它。 我很想知道销毁和重建位置 i 的元素对于所有 i 是否是安全

据我了解,我对此有几个疑问:

  • i 为0 时,我们有点在使用与向量本身存储的相同的initial 指针。因此,我想知道在这种情况下我们是否应该将向量强制为 std::launder 它(是的,这是不可能的,我只是在这里输入 in theory 字段)。

  • 由于这对有一个 const 键,我猜破坏和重建它可能会导致 UB。不过,我对此不太确定,只是我的直觉。

【问题讨论】:

  • 一个问题是,如果您尝试删除最后一个元素(即i == vec.size() - 1),它将无法正常工作。您将销毁最后一个元素,然后尝试从其自身构造它。
  • 你真的需要Key 成为const 吗? vector of const doesn't work that well
  • @1201ProgramAlarm 是的,这只是一个例子,假设有一个if 可以跳过这个案例。
  • @AlanBirtles 我没有,但我为这个问题做。 :)
  • 如果第 2 行或第 3 行中的任何一个抛出,您将被水洗。现在它的析构函数抛出的错误形式(尽管它完全有效),并且移动构造函数也可以抛出。无论哪种情况,当向量最终尝试访问已销毁/未创建的元素(通常是自行销毁)时,都会出现问题。

标签: c++ c++17 language-lawyer undefined-behavior


【解决方案1】:

首先,我只是参考您的这个问题,看看您对 const 成员的关注以及没有std::vectorstd::construct_at 上下文的问题(std::construct 需要进一步的分配器。 ..):

由于该对有一个 const 键,我猜破坏和重建它会导致 UB。不过,我对此不太确定,只是我的直觉。

在 C++20 之前,你是对的,如果旧的引用和指针仍在使用,这将是 UB。但从 C++20 开始,basic.life#8.3 中的相关更改为 n4861/basic.life#8.3

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,如果原始对象是透明的,则可用于操作新对象可被新对象替换(见下文)。对象 o1 可以透明地被对象 o2 替换,如果

  1. o2 占用的存储空间正好覆盖 o1 占用的存储空间,并且
  2. o1 和 o2 属于同一类型(忽略顶级 cv 限定符),并且
  3. o1 不是一个完整的 const 对象,并且
  4. o1 和 o2 都不是可能重叠的子对象,并且
  5. 要么 o1 和 o2 都是完整对象,要么 o1 和 o2 分别是对象 p1 和 p2 的直接子对象,而 p1 是 可以被 p2 透明地替换。

因此,对于这些更改,您的特定(!)示例实际上应该没问题,因为向量元素必须是非常量的,即所引用的对象不能是 const 完整的对象(子点 3)。请记住,这些短语与进一步使用旧引用和指向此重用位置的指针的情况相关!在显式销毁(而不是取消分配)之后进行简单的新放置是先验的,这是进一步的上下文,这与关于 UB 的问题相关。

另见来自

的讨论

Is it possible now with the current C++ standard draft version to define a copy assignment operator for classes with const fields without UB

std::vector-context 与std::construct_at 一起考虑,除了零位情况:

您对常量成员的担忧实际上在这里不再相关,因为我们有一个中间层:分配器(std::vector)和简单连续存储的承诺以及没有“待处理”引用的事实到旧内存(只要您不进一步引用旧的elem 指针...)。否则,标准向量容器上/内部的许多高效操作将是不可能的(将数据放置到预先分配的范围中)。标准库本身使用类似的方案来移动和插入,例如对于许多容器类型。以这种方式规避外部容器的内部内存“假设”仍然非常难看,但对于std::vector 的所有常见实际库实现来说应该没问题(但对于具有复杂内部逻辑的容器(如地图)肯定不行!)。

【讨论】:

  • 所以,从技术上讲,如果我销毁然后重建 pair::first 并且没有引用或指向它的指针,即使在 C++17 中也不会是 UB。我说的对吗?
  • @skypjack 是的。但请始终牢记,毫无疑问,这取决于您使用的标准库实现中对和向量的实际实现!
  • 怎么样?如果他们正确实施了标准并且标准说它不是 UB,那应该没问题,并且永远不要依赖于实际的实施。
  • @SergeBallesta 是的,我建议参考 std::construct 并从向量中提供分配器,也许这就是 OP 的意图?
  • @skypjack 标准库实现可以免费管理其内部。例如,该标准不禁止进一步使用辅助指针,因为我们在常见的映射/树实现中看到了很多。 vector 的 public(!) 接口必须满足几个约束 + 在运行时复杂性和内存处理方面的几个方面。但它不需要关心所有可能的黑客攻击,实际上它不能。更稳健的方法是自己的向量实现存在疑问。
【解决方案2】:

这不是 UB,但它打开了陷阱。在 C++20 之前,所有指向 vec[i] 的指针和引用都会在销毁时消失,因此在操作后使用它们将是 UB。

即使在 C++20 中,指向 vec[i].first 的指针或引用仍然消失,因为 vec[i].first 一个完整的 const 对象,因此它不能透明地替换。

这并不比使用pop_back 然后push_backemplace_back 替换向量的最后一个元素差。但它有完全相同的警告:你在以前的对象存在的地方有一个不同的对象。因此,使用指向 C++17 之前的任何内容或自 C++20 以来声明为 const 的任何内容的引用或指针,显然是 UB。

根本原因是优化编译器喜欢缓存任何声明为 const 的东西。因此,任何以替换 const 对象结尾的技巧只有在您确定永远不会使用指向该该死的 const 事物的直接指针、引用或变量时才是安全的。

当您使用容器尾部时,这一点相当明显,因为该代码的未来用户会意识到某些东西已经消失了。我认为在向量中间这样做更危险,因为它很容易误导未来的维护者或用户。至少它需要一个红色闪烁字体的警告注释。

【讨论】:

  • 所以,您还说在 C++17 中,如果我删除了位置 0 的元素(假设向量可能有指向它的指针),它将是 UB。我理解正确吗?
  • vec[i].first一个完整的 const 对象你确定你知道什么是完整的对象吗?
猜你喜欢
  • 2015-03-29
  • 2016-02-15
  • 2016-02-05
  • 2014-09-14
  • 2015-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多