Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DESTROY called in unexpected sequence

I started out noticing something odd about Scope::Guard.

  • If I undef a $guard variable as the very last statement in a sub, the guard's sub gets called later than I expect.
  • If I don't undef it, or if I do something (anything) after undef $guard, it gets called when the reference goes out of scope as documented. I wonder why.

The code is also to be found here

my $sClass = 'SGuard';
# Uncomment to use Scope::Guard instead:
# use Scope::Guard; $sClass = 'Scope::Guard';

package SGuard;
sub new {
    my ($class, $sub) = @_;
    return bless { sub => $sub }, $class;
}

sub DESTROY {
    my ($self) = @_;
    $self->{sub}->();
}

package main;
sub mySub {
    my $mySubGuard = $sClass->new(sub { 
         print "shouldDestroyFirst\n" 
    });

    # Do something - any no-op will do.
    undef;

    # Comment out this line and it works
    undef $mySubGuard;

    # Or uncomment the next undef line and it works. Any statement(s) or
    # no-ops will do but we test the return value of mySub to make sure it
    # doesn't return a reference, so undef...

    # undef;
}
{
    my $scopeGuard = $sClass->new(sub { 
        print "shouldDestroyLast\n" 
    });

    # Check that mySub returns undef to ensure the reference *did* go out
    # of scope
    printf "mySub returned a %sdefined value\n", defined mySub() ? "" : "un";
}
print "done\n";

In the code I've made my own poor-man's Scope::Guard (SGuard above) just to make the example as simple as possible. You can also use Scope::Guard and get the exact same results that are unexpected at least to me.

I'm expecting that the $mySubGuard inside mySub() should be destroyed first and the $scopeGuard in the scope that calls mySub() should be destroyed last. And so get output like:

 shouldDestroyFirst
 mySub returned a undefined value
 shouldDestroyLast
 done

I get above output if I use undef $mySubGuard line in mySub. If I don't use undef $mySubGuard line in mySub, I get below output:

mySub returned a undefined value
shouldDestroyLast
shouldDestroyFirst
done

So, it looks like the $mySubGuard from mySub() is destroyed after variables local to the outer scope are destroyed.

Why does behavior differ just because I undef a variable that is about to go out of scope anyway? And why does it matter whether something is done afterwards?

like image 243
Peter V. Mørch Avatar asked Jul 11 '12 07:07

Peter V. Mørch


1 Answers

It looks like it's some sort of optimization. If you undef a variable and don't use it afterward, it gets put in some sort of queue to check magic or something. But if you do anything afterwards, it does the DESTROY right then.

Also, there could be a bug that since you are undef-ing it in say a "return context", there is a copy of the variable that's being inspected for something. And perhaps Perl retains a reference which it later cleans up and the end of the calling scope.

You'll also notice that any other statement after undef-ing the guard results in expected behavior. Including the PBP-recommended ending all subs with return. One of Damian's explicit reasons was it avoids unexpected side-effects.

Problem solved: as easily as undef-ing a guard, you can run its handler straight. Don't undef guards as the last (implicit return) statement of a sub. Have a reason to explicitly undef guards, such as you want to run their handler right then, to do further processing.

It's confusing and unexpected, but definitely not something that should arise in finished or standardized code.

like image 130
Axeman Avatar answered Sep 29 '22 01:09

Axeman