Goto and Loops - NOT a goto discussion

Started by
19 comments, last by alvaro 11 years, 3 months ago

First - this isn't meant as a question whether using gotos is appropriate to use in an OO langauge. That's been overdone, so please don't let it degenerate to that type of discussion.

I DID however use a goto recently in a nested loop to escape it, and it got me thinking about the variables used in a loop, and the flow.

I tested the following minimal, working, c++ code:


#include <iostream>
using namespace std;

int main ()
{
  if (true)
  {
    for ( int i = 0 ; i < 3; i++)
    {
    looped:
      cout << i << " ";
    }
  }
  cout << "\nexit loop\n";
  int j = -1;
  // cout << i; // does not compile, obviously as expected
  goto looped;
}

This actually ends up printing:

0
1
2
exit loop
3
exit loop
4
exit loop
5
exit loop
6

And so on, forever

So... I expected i to be popped off the stack once the loop exited naturally (the first time), and the declaration of j to replace it on the stack.

I'm using Visual Studio 2012 Express to test this. I haven't tested it on anything else yet. Why does it look like the declaration of i isn't popped off the stack at all?

This also makes me wonder what happens when I use a goto to escape the loop - does the variable declared in the loop's header stay on the stack? It seems like it should, but after this experiment, I'm not feeling too confident in how I know the stack to work.

Anyone have any comments?

Advertisement
I'll take a punt on "undefined behaviour". Typically the stack pointer is set up only once per function, not once per scope (i.e. the curly brackets), and the compiler could share the same stack location for i and j, maybe the goto has made it realise you jump into another scope so it doesn't? Did you test it in release mode/optimisations on as well?

It definitely wouldn't work like that if there was a nontrivial destructor for i...
"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley
Why does it look like the declaration of i isn't popped off the stack at all?
Because you never left the function. The stack pointer won’t be modified until you enter or exit a function.
And what you are seeing is undefined behavior. When it goes back into the loop it simply takes the value at the address of “i” and keeps working on it.

Not to confuse you further, but nothing really “leaves” the stack. It is just a series of addresses and a pointer to somewhere in them that gets shifted up and down as you enter/leave functions.
The pointer gets shifted but the data all over the stack is not changed when this happens. Through undefined behavior and hacks you can access values on the stack from the caller of your current function, for example. You can also modify values “ahead” of your current stack position and change data that the next function you call will use.

You are basically associating scopes with stacks. This is wrong. Scopes are a language constraint. Just because a variable goes out of scope does not mean the stack gets modified in any way (destructors can cause this to happen though). Nothing happens to “i” just because you leave the for-loop.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

I'll take a punt on "undefined behaviour". Typically the stack pointer is set up only once per function, not once per scope (i.e. the curly brackets), and the compiler could share the same stack location for i and j, maybe the goto has made it realise you jump into another scope so it doesn't? Did you test it in release mode/optimisations on as well?

It definitely wouldn't work like that if there was a nontrivial destructor for i...

I was testing this in release mode - as for optimizations /OPT:REF is set, but that's only supposed to be to eliminate variables that are never referenced, not to keep variables like that.

But then, based on your last line, I tried this code (also fixing my lack of return statement in main... shame on me :) )


#include <iostream>
using namespace std;

class Nontrivial
{
public:
Nontrivial () { i = 0; }
~Nontrivial () { i = -100; }
int i;
};

int main ()
{
  if (true)
  {
    for ( Nontrivial n; n.i < 3; n.i++)
    {
      looped:
      cout << n.i << " ";
    }
  }
  cout << "\nexit loop\n";
  int j = -1;
  // cout << i; // does not compile
  goto looped;
  return 0;
}

This.... didn't compile at all:

Error 1 error C2362: initialization of 'n' is skipped by 'goto looped'

Yeah....

This should be totally well-defined AFAIK.

The lifetime of the stack slot is an implementation detail; the language semantics only care about the lifetime of the data placed in that slot. If you place a variable definition inside a loop, it'd be wasteful to push and pop the stack every loop iteration to reserve stack space for the variable; as an optimization, it is common to reserve all needed stack space in a single shot when entering a function, and clean it all up in one pop at the end. (Note that this depends on calling conventions and other details.)

If you put an object with nontrivial construction/destruction in place of a primitive, the compiler will construct and destruct it on scope entry/exit as you would expect. It will typically reuse stack slots as far as is safe, but since you can easily construct cases that can't be statically proven safe, the compiler will often not reuse stack slots specifically to avoid mashing two things into the same slot.


[edit] The reason this fails with your nontrivial example is that the compiler can't statically figure out how to safely construct the object for every possible flow scenario.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

Actually, this is a should-have-failed-to-compile situation according to the standard. If you jump into a block where a variable with automatic storage duration with an initializer is in scope from a place where it isn't in scope, the program is ill-formed. (Relevant section is 6.7 of all of C++98, C++03 and C++11.) MSVC permits this as an extension. If you disable language extensions it fails to compile with a C2362.
Why does it look like the declaration of i isn't popped off the stack at all?
Because you never left the function. The stack pointer won’t be modified until you enter or exit a function.
And what you are seeing is undefined behavior. When it goes back into the loop it simply takes the value at the address of “i” and keeps working on it.

Not to confuse you further, but nothing really “leaves” the stack. It is just a series of addresses and a pointer to somewhere in them that gets shifted up and down as you enter/leave functions.
The pointer gets shifted but the data all over the stack is not changed when this happens. Through undefined behavior and hacks you can access values on the stack from the caller of your current function, for example. You can also modify values “ahead” of your current stack position and change data that the next function you call will use.

You are basically associating scopes with stacks. This is wrong. Scopes are a language constraint. Just because a variable goes out of scope does not mean the stack gets modified in any way (destructors can cause this to happen though). Nothing happens to “i” just because you leave the for-loop.


L. Spiro

Ok, I understand that nothing "leaves" the stack, no confusion there :). That's why I had put in the declaration of j. But the fact that the stack is allocated/deallocated on a per-function basis would actually explain this, I suppose.

main is the only function that returns a value that you are allowed to skip the return statement ;) It implicitly returns 0 if you omit it.
"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley
Actually, this is a should-have-failed-to-compile situation according to the standard. If you jump into a block where a variable with automatic storage duration with an initializer is in scope from a place where it isn't in scope, the program is ill-formed. (Relevant section is 6.7 of all of C++98, C++03 and C++11.) MSVC permits this as an extension. If you disable language extensions it fails to compile with a C2362.

Yes, you're correct, disabling language extensions I get the "initialization skipped" error.

Oddly however, I left the loop definition like this, without initializing i to 0:


for ( int i; i < 3; i++)

and it compiled. Value of i was obviously random, but it compiled and ran just fine, printing a random number and continuously incrementing it.

I don't know why I had the idea that the stack was dealt with inside a function, instead of only on a function level. I suppose language syntax and scoping rules made me assume it worked like that, but in retrospect, I don't think I've read anything to indicate it.

Welcome to undefined behavior: stuff that makes zero sense, compiles, runs, and might (if you're lucky) indicate something is broken before it's too late.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

This topic is closed to new replies.

Advertisement