2

Suppose I have a lot of stack allocations in one method.

If I add curly braces to one set of code, will the allocated objects be popped off of the stack when they go out of scope, or will the method need to exit before the memory is released?

I should also add that this is being done in an MFC application.

void LongMethod()
{
    {
        struct someLargeStruct;
        // Do a lot of work and allocations.
    }

    {
        struct anotherLargeStruct;
        // more work here.
    }   
}
netcat
  • 1,281
  • 1
  • 11
  • 21
  • 9
    There is no general way of answering this question (who says the target platform even has a stack?). Look at the code generated by your compiler. – melpomene Feb 21 '19 at 16:02
  • 1
    Why not just make those separate scopes separate functions? – NathanOliver Feb 21 '19 at 16:02
  • 3
    It is implementation defined because the C++ standard (and the C one) don't know about the call stack. – Basile Starynkevitch Feb 21 '19 at 16:02
  • 3
    While the stack is a limited resource, if you're worrying about passing that limit then perhaps the solution to your problem isn't more nesting or more functions. Perhaps the problem is on a higher level (design, analysis or even requirements), and the solution have to be on that level? – Some programmer dude Feb 21 '19 at 16:04
  • 5
    The scopes (curly braces) ensure that object destructors get called when the scope ends. Whether or not memory is also released is implementation defined and you usually shouldn't need to care. – Jesper Juhl Feb 21 '19 at 16:05
  • 2
    I'm doing a lot of bit-shifting on data packets. The design is completely up to me, but it makes sense to do everything in one or maybe two methods. I suppose I could use the heap. So far everything works correctly. – netcat Feb 21 '19 at 16:06
  • 1
    Thanks Jesper. Good info. – netcat Feb 21 '19 at 16:08
  • 1
    How large are your structs? A few kilobytes, A few megabytes? If it's a few kilobytes, you don't need to worry. If it's Megabytes then you might need to worry. You can also increase the stack size of your executable (platform dependent). Using the heap may or may not be a good solution, it totally depends on the context – Jabberwocky Feb 21 '19 at 16:13
  • Unless it's deeply recursive, @Jabberwocky. A few KB per stack frame times hundreds of stack frames brings you right into that megabyte range. – John Bollinger Feb 21 '19 at 16:23
  • @JohnBollinger good point, but recursive calls with large local objects is probably a bad idea anyway. – Jabberwocky Feb 21 '19 at 16:24
  • Thanks for all the good comments. There seems to be no issue in MFC, that platform that this is on. I will refactor the code, but right now I am just trying to finish this project. – netcat Feb 21 '19 at 16:58
  • Thanks rustyx, I removed the C tag. – netcat Feb 21 '19 at 16:58

2 Answers2

3

There is no stack in C++ (at least, not in the way you think of it, there is std::stack container adapter, but it is completely different).

However, there is an automatic storage, which often is implemented on the stack, but could be not (for example, it could be in register).

However, for automatic storage,

The storage for the object is allocated at the beginning of the enclosing code block and deallocated at the end. All local objects have this storage duration, except those declared static, extern or thread_local.

(https://en.cppreference.com/w/cpp/language/storage_duration)

Which means that if compiler decides to use stack to store variables, it should know that their storage ends at the end of the enclosing block. And although compiler is under no obligation to decrease stack pointer at this point, it would be a serious quality of implementation issue not to.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
3

Just to clarify this a little further - yes the standard requires automatic storage to be freed at the end of the block scope, see [basic.stc.auto]/1:

Block-scope variables explicitly declared register or not explicitly declared static or extern have automatic storage duration. The storage for these entities lasts until the block in which they are created exits.

However, the compiler is required to implement only the observable behavior of the program (the as-if rule):

...implementations are required to emulate (only) the observable behavior of the abstract machine...

Since the standard treats automatic storage as unlimited and there is no other way to observe stack usage, under the as-if rule a conforming compiler is not obliged to be strict about freeing memory exactly at the end of each scope.

Indeed we observe that GCC, clang and MSVC prefer to allocate stack space once upon function start and deallocate at function exit. Although at least they seem to re-use memory between different block scopes:

int do_something_with(int*);
int do_something_with(long long*);

void do_something()
{
    {
        int x[100];
        do_something_with(x);
    }
    {
        long long y[100];
        do_something_with(y);
    }
}

Stack frame allocated: 800 bytes (x and y share the same space):

do_something():
        sub     rsp, 808
        mov     rdi, rsp
        call    do_something_with(int*)
        mov     rdi, rsp
        call    do_something_with(long long*)
        add     rsp, 808
        ret

Without the block-scopes the behavior changes slightly, we observe no memory re-use anymore:

int do_something_with(int*);
int do_something_with(long long*);

void do_something()
{
    int x[100];
    do_something_with(x);
    long long y[100];
    do_something_with(y);
}

Stack frame allocated: 1200 bytes (x + y):

do_something():
        sub     rsp, 1208
        mov     rdi, rsp
        call    do_something_with(int*)
        lea     rdi, [rsp+400]
        call    do_something_with(long long*)
        add     rsp, 1208
        ret

So to conclude, yes, there is some effect of the block-scopes, but don't expect it to be exactly aligned to those scopes.

It can be especially vexing in recursive functions (example):

int do_something_with(long long*);
int bar();

void foo()
{
    {
        if (bar())
            foo(); // don't count on this being efficient
    }
    {
        long long y[10000];
        do_something_with(y);
    }
}

It's much safer therefore to isolate heavy stack users into separate functions.

rustyx
  • 80,671
  • 25
  • 200
  • 267