std::vector (ab) 使用自动存储

Igo*_* R. 44 c++ libstdc++ stdvector language-lawyer automatic-storage

考虑以下片段:

#include <array>
int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  huge_type t;
}
Run Code Online (Sandbox Code Playgroud)

显然它会在大多数平台上崩溃,因为默认堆栈大小通常小于 20MB。

现在考虑以下代码:

#include <array>
#include <vector>

int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  std::vector<huge_type> v(1);
}
Run Code Online (Sandbox Code Playgroud)

令人惊讶的是它也崩溃了!回溯(使用最近的 libstdc++ 版本之一)指向include/bits/stl_uninitialized.h文件,我们可以在其中看到以下几行:

typedef typename iterator_traits<_ForwardIterator>::value_type _ValueType;
std::fill(__first, __last, _ValueType());
Run Code Online (Sandbox Code Playgroud)

调整大小vector构造函数必须默认初始化元素,这就是它的实现方式。显然,_ValueType()临时会导致堆栈崩溃。

问题是它是否是一个符合要求的实现。如果是,那实际上意味着大类型向量的使用非常有限,不是吗?

Yak*_*ont 19

任何标准 API 使用的自动存储量没有限制。

它们都可能需要 12 TB 的堆栈空间。

但是,该 API 只需要Cpp17DefaultInsertable,并且您的实现会在构造函数所需的内容上创建一个额外的实例。除非它在检测对象之后进行门控是微不足道的可操作和可复制的,否则该实现看起来是非法的。

  • 从 libstdc++ 代码来看,只有当元素类型很简单并且可复制分配并且使用默认的“std::allocator”时,才使用此实现。我不知道为什么要提出这个特殊情况。 (8认同)
  • 是的,我想可以,但是对于大元素,GCC 似乎不行。Clang 和 libstdc++ 确实优化了临时值,但似乎只有传递给构造函数的向量大小是编译时常量时,请参阅 https://godbolt.org/z/-2ZDMm。 (4认同)
  • @walnut 这意味着编译器可以自由地创建该临时对象,就好像没有实际创建该临时对象一样;我猜优化构建很有可能没有被创建? (3认同)
  • @walnut 有一个特殊情况,因此我们将简单类型分派到 `std::fill` ,然后使用 `memcpy` 将字节爆炸到合适的位置,这可能比在循环中构造大量单独的对象要快得多。我相信 libstdc++ 实现是一致的,但导致巨大对象的堆栈溢出是一个实现质量 (QoI) 错误。我已将其报告为 https://gcc.gnu.org/PR94540 并将修复它。 (2认同)

eer*_*ika 9

huge_type t;
Run Code Online (Sandbox Code Playgroud)

显然它会在大多数平台上崩溃......

我对“大多数”的假设提出异议。由于巨大对象的内存从来没有被使用过,编译器可以完全忽略它,从不分配内存,这样就不会发生崩溃。

问题是它是否是一个符合要求的实现。

C++ 标准不限制堆栈的使用,甚至不承认堆栈的存在。所以,是的,它符合标准。但人们可以认为这是一个实施质量问题。

它实际上意味着使用巨大类型的向量非常有限,不是吗?

libstdc++ 似乎就是这种情况。崩溃不是用 libc++(使用 clang)重现的,所以这似乎不是语言的限制,而只是在那个特定的实现中。

  • _“尽管堆栈溢出,但不一定会崩溃,因为程序永远不会访问分配的内存”_ - 如果在此之后以任何方式使用堆栈(例如调用函数),即使在溢出时也会崩溃提交平台。 (6认同)

Adr*_*thy 5

我不是语言律师,也不是 C++ 标准专家,但 cppreference.com 说:

explicit vector( size_type count, const Allocator& alloc = Allocator() );

使用 count 个默认插入的 T 实例构造容器。不制作副本。

也许我误解了“默认插入”,但我希望:

std::vector<huge_type> v(1);
Run Code Online (Sandbox Code Playgroud)

相当于

std::vector<huge_type> v;
v.emplace_back();
Run Code Online (Sandbox Code Playgroud)

后一个版本不应该创建堆栈副本,而是直接在向量的动态内存中构造一个巨大的类型。

我不能权威地说你所看到的是不合规的,但这肯定不是我对高质量实施的期望。

  • 正如我在对该问题的评论中提到的,libstdc++ 仅将此实现用于具有复制分配和“std::allocator”的简单类型,因此直接插入向量内存和创建中间副本之间应该没有明显的差异。 (4认同)
  • 是的我同意。我认为这是实施过程中的疏忽。我的观点只是,就标准合规性而言并不重要。 (2认同)