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


46

考虑以下代码段:

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

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

现在考虑以下代码:

#include <array>
#include <vector>

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

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

typedef typename iterator_traits<_ForwardIterator>::value_type _ValueType;
std::fill(__first, __last, _ValueType());

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

问题是这是否是符合标准的实现。如果是,那么实际上意味着使用巨大类型的向量是非常有限的,不是吗?


一个人不应该以数组类型存储大对象。这样做可能需要很大的连续内存区域,而该区域可能不存在。相反,要有一个指针向量(通常是std :: unique_ptr),这样就不会对内存有如此高的要求。
NathanOliver

2
只是记忆。有一些运行的C ++实现不使用虚拟内存。
NathanOliver

3
顺便说一下,哪个编译器?我无法用VS 2019重现(16.4.2)
ChrisMM

3
通过查看libstdc ++代码,仅当元素类型为琐碎且可分配副本且使用默认值时,才使用此实现std::allocator
胡桃

1
@Damon正如我上面提到的,它似乎仅用于带有默认分配器的普通类型,因此不应有任何明显的区别。
胡桃

Answers:


19

对于任何std API使用多少自动存储没有限制。

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

但是,该API仅需要Cpp17DefaultInsertable,并且您的实现会在构造函数所需的范围之外创建一个额外的实例。除非在检测到对象是可微改的和可复制的对象之后进行门控,否则该实现看起来是非法的。


8
通过查看libstdc ++代码,仅当元素类型为琐碎且可分配副本且使用默认值时,才使用此实现std::allocator。我不确定为什么首先要提出这种特殊情况。
胡桃

3
@walnut这意味着编译器可以自由地创建而不是实际创建该临时对象。我猜没有创建的优化版本有很大的机会吗?
Yakk-Adam Nevraumont

4
是的,我想可以,但是对于大型元素,GCC似乎没有。带有libstdc ++的Clang确实优化了临时属性,但是似乎只有传递给构造函数的向量大小是编译时常量,请参见godbolt.org/z/-2ZDMm
胡桃

1
@walnut是一种特殊情况,因此我们可以std::fill将琐碎的类型分派到,然后将其memcpy用于将字节爆破到位,这可能比在循环中构造许多单个对象要快得多。我相信libstdc ++实现是符合标准的,但是导致大型对象的堆栈溢出是实现质量(QoI)错误。我已将其报告为gcc.gnu.org/PR94540,并将对其进行修复。
Jonathan Wakely

@JonathanWakely是的,这很有道理。我不记得为什么在写评论时没有想到这一点。我猜想我会想到,第一个默认构造的元素将直接就地构造,然后可以从中进行复制,这样就不会构造任何其他类型的对象。但是,当然,我还没有真正详细地考虑过这一点,我也不知道实现标准库的来龙去脉。(我为时已晚,这也是您在错误报告中的建议。)
胡桃木

9
huge_type t;

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

我对“多数”的假设提出异议。由于从未使用过巨大对象的内存,因此编译器可以完全忽略它,也不会分配内存,在这种情况下不会发生崩溃。

问题是这是否是符合标准的实现。

C ++标准不限制堆栈的使用,甚至不承认堆栈的存在。因此,是的,它符合标准。但是可以认为这是实施问题。

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

libstdc ++就是这种情况。崩溃不是使用libc ++(使用clang)重现的,因此似乎这不是语言的限制,而是仅限于该特定实现。


6
“即使堆栈溢出也不一定会崩溃,因为分配的内存永远不会被程序访问” –如果在此之后以任何方式使用堆栈(例如,调用函数),即使在过量使用的平台上也会崩溃。
Ruslan

不会崩溃(假设未成功分配对象)的任何平台都容易受到堆栈冲突的影响。
user253751

@ user253751乐观地认为大多数平台/程序都不容易受到攻击。
eerorika

我认为过量使用仅适用于堆,不适用于堆栈。堆栈的大小上限是固定的。
乔纳森·威克利

@JonathanWakely你是对的。看来它之所以不会崩溃,是因为编译器从不分配未使用的对象。
eerorika

5

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

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

使用count缺省插入的T实例构造容器。不进行任何复制。

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

std::vector<huge_type> v(1);

相当于

std::vector<huge_type> v;
v.emplace_back();

后者不应创建堆栈副本,而应直接在向量的动态内存中构造一个huge_type。

我不能权威地说您看到的是不合规的,但肯定不是我期望从质量实现中获得的。


4
正如我在对该问题的评论中提到的那样,libstdc ++仅将这种实现用于带拷贝分配和的琐碎类型std::allocator,因此,直接插入向量存储器和创建中间拷贝之间应该没有明显的区别。
胡桃

@walnut:是的,但是对于高质量的实现,我仍然无法期望巨大的堆栈分配以及init和copy对性能的影响。
Adrian McCarthy

2
是的我同意。我认为这是实施过程中的疏忽。我的意思是,就标准合规性而言,这无关紧要。
胡桃

IIRC您还需要可复制性或可移动性,emplace_back但不仅仅是创建矢量。这意味着您可以拥有,vector<mutex> v(1)但不能拥有。vector<mutex> v; v.emplace_back();对于类似的东西huge_type,第二个版本可能仍具有分配和移动操作。两者都不应该创建临时对象。
dyp

1
@IgorR。vector::vector(size_type, Allocator const&)要求(Cpp17)DefaultInsertable
dyp
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.