44

Consider the following snippet:

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

Obviously it would crash on most of platforms, because the default stack size is usually less than 20MB.

Now consider the following code:

#include <array>
#include <vector>

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

Surprisingly it also crashes! The traceback (with one of the recent libstdc++ versions) leads to include/bits/stl_uninitialized.h file, where we can see the following lines:

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

The resizing vector constructor must default-initialize the elements, and this is how it's implemented. Obviously, _ValueType() temporary crashes the stack.

The question is whether it's a conforming implementation. If yes, it actually means that the use of a vector of huge types is quite limited, isn't it?

Igor R.
  • 14,716
  • 2
  • 49
  • 83
  • One should not store huge objects in a array type. Doing so potentially requires a very large region of contigious memory that may not be present. Instead, have a vector of pointers (std::unique_ptr typically) so that you don't place such a high demand on your memory. – NathanOliver Jan 06 '20 at 20:31
  • @NathanOliver contiguous *virtual* memory you mean. – Igor R. Jan 06 '20 at 20:32
  • 2
    Just memory. There are C++ implementations running that don't use virtual memory. – NathanOliver Jan 06 '20 at 20:36
  • @NathanOliver the are C++ implementations that do not use stack as well :). I mean that in my use-case requesting a few tens of MB (and even more) of cont.memory is ok. – Igor R. Jan 06 '20 at 20:39
  • 3
    Which compiler, btw? I can't reproduce with VS 2019 (16.4.2) – ChrisMM Jan 06 '20 at 20:50
  • 3
    From looking at the libstdc++ code, this implementation is only used if the element type is trivial and copy assignable and if the default `std::allocator` is used. – walnut Jan 06 '20 at 20:54
  • @CrisMM I mentioned that it's libstdc++, i.e. GCC. – Igor R. Jan 06 '20 at 20:58
  • @walnut right, so passing a custom allocator could probably help. But this looks like a hack... – Igor R. Jan 06 '20 at 21:00
  • If that is indeed the implementation that's used, I daresay it's non-conforming, since it value-assigns (which is what `std::fill` does) when the standard clearly says "default construct" in the constructor for `std::vector`. Be sure to check you've 100% certain found the correct code though. Can be tricky to find, and possibly it's something else. – Damon Jan 06 '20 at 22:40
  • 2
    @Damon As I mentioned above it seems to only be used for trivial types with the default allocator, so there shouldn't be any observable difference. – walnut Jan 06 '20 at 23:52
  • @Damon it's the code you can directly see in the context of the segfault. – Ruslan Jan 07 '20 at 07:57
  • @walnut: One observable difference would be _copying_ data when no copying is necessary (the implicitly-defined default constructor has an empty initializer list, i.e. it's factually a no-op). Another obvious observable difference is the very crash which comes from creating a temporary when no temporary is to be created. – Damon Jan 07 '20 at 18:19
  • 1
    @Damon The former is not part of the [*observable behavior*](https://timsong-cpp.github.io/cppwp/n4659/intro#execution-7) of the program and an implementation of the standard may do whatever it wants as long as the observable behavior is the same, see the [*as-if rule*](https://timsong-cpp.github.io/cppwp/n4659/intro#execution-1). The latter should be covered by the standard not setting any memory requirements on library calls and by the implementation limit rules, see answers to the question. – walnut Jan 07 '20 at 18:28
  • I'm wondering whether the downvotes are on purpose or just misclicks... :-) – Igor R. Jan 07 '20 at 19:45

3 Answers3

19

There is no limit on how much automatic storage any std API uses.

They could all require 12 terabytes of stack space.

However, that API only requires Cpp17DefaultInsertable, and your implementation creates an extra instance over what is required by the constructor. Unless it is gated behind detecting the object is trivially ctorable and copyable, that implementation looks illegal.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 8
    From looking at the libstdc++ code, this implementation is only used if the element type is trivial and copy assignable and if the default `std::allocator` is used. I am not sure why this special case is made in the first place. – walnut Jan 06 '20 at 20:54
  • 3
    @walnut Which means the compiler is free to as-if not actually create that temporary object; I'm guessing there is a decent chance on an optimized build it doesn't get created? – Yakk - Adam Nevraumont Jan 07 '20 at 14:05
  • 4
    Yes, I guess it could, but for large elements GCC doesn't seem to. Clang with libstdc++ does optimize out the temporary, but it seems only if the vector size passed to the constructor is a compile-time constant, see https://godbolt.org/z/-2ZDMm. – walnut Jan 07 '20 at 17:33
  • 2
    @walnut the special case is there so that we dispatch to `std::fill` for trivial types, which then uses `memcpy` to blast the bytes into places, which is potentially much faster than constructing lots of individual objects in a loop. I believe the libstdc++ implementation is conforming, but causing a stack overflow for huge objects is a Quality of Implementation (QoI) bug. I've reported it as https://gcc.gnu.org/PR94540 and will fix it. – Jonathan Wakely Apr 09 '20 at 14:56
  • @JonathanWakely Yes, that makes sense. I don't remember why I didn't think of that when I wrote my comment. I guess I would have thought that the first default-constructed element would be constructed directly in-place and then one could copy from that, so that no additional objects of the element type will ever be constructed. But of course I have not really thought this through in detail and I don't know the in and outs of implementing the standard library. (I realized too late that this is also your suggestion in the bug report.) – walnut Apr 09 '20 at 15:20
9
huge_type t;

Obviously it would crash on most of platforms ...

I dispute the assumption of "most". Since the memory of the huge object is never used, the compiler can completely ignore it and never allocate the memory in which case there would be no crash.

The question is whether it's a conforming implementation.

The C++ standard doesn't limit stack use, or even acknowledge the existence of a stack. So, yes it conforms to the standard. But one could consider this to be a quality of implementation issue.

it actually means that the use of a vector of huge types is quite limited, isn't it?

That appears to be the case with libstdc++. The crash was not reproduced with libc++ (using clang), so it seems that this is not limitation in the language, but rather only in that particular implementation.

Community
  • 1
  • 1
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • 6
    _"won't necessarily crash despite overflowing of stack because the allocated memory is never accessed by the program"_ — if the stack is used in any way after this (e.g. to call a function), this will crash even on the over-committing platforms. – Ruslan Jan 07 '20 at 07:56
  • Any platform on which this does not crash (assuming the object isn't successfully allocated) is vulnerable to Stack Clash. – user253751 Jan 07 '20 at 17:51
  • @user253751 It would be optimistic to assume that most platforms / programs aren't vulnerable. – eerorika Jan 07 '20 at 18:03
  • I think overcommit only applies to the heap, not the stack. The stack has a fixed upper bound on its size. – Jonathan Wakely Jan 21 '20 at 12:14
  • @JonathanWakely You're right. It appears that the reason why it doesn't crash is because the compiler never allocates the object that is unused. – eerorika Jan 21 '20 at 13:53
5

I'm not a language lawyer nor a C++ standard expert, but cppreference.com says:

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

Constructs the container with count default-inserted instances of T. No copies are made.

Perhaps I'm misunderstanding "default-inserted," but I would expect:

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

to be equivalent to

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

The latter version shouldn't create a stack copy but construct a huge_type directly in the vector's dynamic memory.

I can't authoritatively say that what you're seeing is non-compliant, but it's certainly not what I would expect from a quality implementation.

Adrian McCarthy
  • 45,555
  • 16
  • 123
  • 175
  • 4
    As I mentioned in a comment on the question, libstdc++ only uses this implementation for trivial types with copy assignment and `std::allocator`, so there should be no observable difference between inserting directly into the vectors memory and creating an intermediate copy. – walnut Jan 06 '20 at 23:55
  • @walnut: Right, but the huge stack allocation and the performance impact of init and copy are still things I wouldn't expect from a high-quality implementation. – Adrian McCarthy Jan 07 '20 at 18:32
  • 2
    Yes, I agree. I think this was an oversight in the implementation. My point was only that it doesn't matter in terms of standard compliance. – walnut Jan 07 '20 at 18:36
  • IIRC you also need copyability or movability for `emplace_back` but not for just creating a vector. Which means you can have `vector v(1)` but not `vector v; v.emplace_back();` For something like `huge_type` you might still have an allocation and move operation more with the second version. Neither should create temporary objects. – dyp Jan 07 '20 at 21:25
  • @dyp No, the both require [MoveInsertable](https://en.cppreference.com/w/cpp/named_req/MoveInsertable) only. – Igor R. Jan 08 '20 at 19:49
  • 1
    @IgorR. `vector::vector(size_type, Allocator const&)` requires (Cpp17)DefaultInsertable – dyp Jan 10 '20 at 09:53