Scope unwinding in PHP class constructors
Asked Answered
T

4

10

I'm learning PHP classes and exceptions, and, coming from a C++ background, the following strikes me as odd:

When the constructor of a derived class throws an exception, it appears that the destructor of the base class is not run automatically:

class Base
{
  public function __construct() { print("Base const.\n"); }
  public function __destruct()  { print("Base destr.\n"); }
}

class Der extends Base
{
  public function __construct()
  {
    parent::__construct();
    $this->foo = new Foo;
    print("Der const.\n");
    throw new Exception("foo"); // #1
  }
  public function __destruct()  { print("Der destr.\n"); parent::__destruct(); }
  public $foo;                  // #2
}

class Foo
{
  public function __construct() { print("Foo const.\n"); }
  public function __destruct()  { print("Foo destr.\n"); }
}


try {
  $x = new Der;
} catch (Exception $e) {
}

This prints:

Base const.
Foo const.
Der const.
Foo destr.

On the other hand, the destructors of member objects are executed properly if there is an exception in the constructor (at #1). Now I wonder: How do you implement correct scope unwinding in a class hierarchy in PHP, so that subobjects are properly destroyed in the event of an exception?

Also, it seems that there's no way to run the base destructor after all the member objects have been destroyed (at #2). To wit, if we remove line #1, we get:

Base const.
Foo const.
Der const.
Der destr.
Base destr.
Foo destr.    // ouch!!

How would one solve that problem?

Update: I'm still open to further contributions. If someone has a good justification why the PHP object system never requires a correct destruction sequence, I'll give out another bounty for that (or just for any other convincingly argued answer).

Torsibility answered 19/9, 2011 at 12:31 Comment(6)
I must say that I've very very rarely needed to implement destructors in PHP, so perhaps it's not much of an issue. You do raise a good question though.Hijoung
@Jani: Frankly, given the way they're designed, I understand why you really woudln't want to use destructors. I just wonder why they appear to be so poorly thought out, and whether there's any common idiom to circumvent this design flaw other than "don't use this part of the language"... :-STorsibility
Agree with Jani: in PHP there's really no point in writing destructors because there's nothing you can leak. Coming from C++, you are probably viewing destructors as a very badly thought out tool for solving a problem that they are not meant to solve.Ceja
@Jon: Could you clarify please? Do you mean that destructors are a well-thought out concept that is useful and works when used idiomatically, or do you mean that a well-designed PHP program should not use destructors? If you have a good argument for the former, please do post it as an answer!Torsibility
The latter, and I would not say "should not use destructors" because it is perhaps too strong and I 'm only judging from experience: in the 10 years I 've been writing PHP, I have never needed to write a single one. I would like to see an answer of the first type as much as you do.Ceja
If you need Base and Der destructors, make sure they are available.Rudich
C
6

I would like to explain why PHP behaves this way and why it actually makes (some) sense.

In PHP an object is destroyed as soon as there are no more references to it. A reference can be removed in a multitude of ways, e.g. by unset()ing a variable, by leaving scope or as part of shutdown.

If you understood this, you can easily understand what happens here (I'll explain the case without the Exception first):

  1. PHP enters shutdown, thus all variable references are removed.
  2. When the reference created by $x (to the instance of Der) is removed the object is destroyed.
  3. The derived destructor is called, which calls the base destructor.
  4. Now the reference from $this->foo to the Foo instance is removed (as part of destroying the member fields.)
  5. There aren't any more references to Foo either, so it is destroyed too and the destructor is called.

Imagine this would not work this way and member fields would be destroyed before calling the destructor: You couldn't access them anymore in the destructor. I seriously doubt that there is such a behavior in C++.

In the Exception case you need to understand that for PHP there never really existed an instance of the class, as the constructor never returned. How can you destruct something that was never constructed?


How do I fix it?

You don't. The mere fact that you need a destructor probably is a sign of bad design. And the fact that the destruction order matters that much to you, is even more.

Cogon answered 23/9, 2011 at 6:42 Comment(7)
I'm not sure I buy this explanation: When I say ouch in the example, it's because I expected the destruction sequence to be "derived - foo - base"; but of course that doesn't happen because I actually call the base destructor explicitly. But imagine the $this->foo object depended somehow on the valid state of the Base subobject. Now the destruction of $this->foo may need perform some shutdown that requires the Base subobject, but that's no longer valid. Is there a reason why this cannot or should not happen?Torsibility
@KerrekSB I already explained that the members need to be destroyed after the destructor is called, otherwise you can't access them in the destructor. This is same as in C++ (afaik). Furthermore: $this->foo mustn't depend on the Base object (how should it actually access it?). $this->foo is a dependency (that ought to be injected, see DI, IoC and SOLID). It shouldn't even know that it is used in another class and definitely not rely on that.Cogon
The distinction between base and derived object is important. Indeed, the derived destructor has to come first. But $this->foo was constructed after the base subobject was constructed, so it should be destroyed before the base, non? I'll add an example to the post!Torsibility
I've marked the proposed base dependency as line #3. But after some testing I found that you cannot capture parent by reference in Bar's constructor, so perhaps the language simply does not allow any references to the Base subobject to be obtained by member objects?Torsibility
@KerrekSB Sorry, I can't follow anymore. What do you mean by "parent" in that code? What shall it do? (Or can you give me a reference for that syntax in C++ so I can read it up?)Cogon
I'll post a more detailed explanation in a separate "answer" post here, to keep the main post clean.Torsibility
@KerrekSB Having read through your answer I really think you should reconsider your design. But apart from that: You obviously still have the possibility to explicitly remove references (and thus trigger the destructor, if there are no further references): __destruct() { 'derived destructor code'; unset($this->foo); parent::__destruct(); }Cogon
T
2

This is not an answer, but rather a more detailed explanation of the motivation for the question. I don't want to clutter the question itself with this somewhat tangential material.

Here is an explanation of how I would have expected the usual destruction sequence of a derived class with members. Suppose the class is this:

class Base
{
  public $x;
  // ... (constructor, destructor)
}

class Derived extends Base
{
  public $foo;
  // ... (constructor, destructor)
}

When I create an instance, $z = new Derived;, then this first constructs the Base subobject, then the member objects of Derived (namely $z->foo), and finally the constructor of Derived executes.

Therefore, I expected the destruction sequence to occur in the exact opposite order:

  1. execute Derived destructor

  2. destroy member objects of Derived

  3. execute Base destructor.

However, since PHP does not call base destructors or base constructors implicitly, this doesn't work, and we have to make the base destructor call explicit inside the derived destructor. But that upsets the destruction sequence, which is now "derived", "base", "members".

Here's my concern: If any of the member objects require the state of the base subobject to be valid for their own operation, then none of these member objects can rely on that base subobject during their own destruction, because that base object has already been invalidated.

Is this a genuine concern, or is there something in the language that prevents such dependencies from happening?

Here is an example in C++ which demonstrates the need for the correct destruction sequence:

class ResourceController
{
  Foo & resource;
public:
  ResourceController(Foo & rc) : resource(rc) { }
  ~ResourceController() { resource.do_important_cleanup(); }
};

class Base
{
protected:
  Foo important_resource;
public:
  Base() { important_resource.initialize(); }  // constructor
  ~Base() { important_resource.free(); }       // destructor
}

class Derived
{
  ResourceController rc;
public:
  Derived() : Base(), rc(important_resource) { }
  ~Derived() { }
};

When I instantiate Derived x;, then the base subobject is constructed first, which sets up important_resource. Then the member object rc is initialized with a reference to important_resource, which is required during rc's destruction. So when the lifetime of x ends, the derived destructor is called first (doing nothing), then rc is destroyed, doing its cleanup job, and only then is the Base subobject destroyed, releasing important_resource.

If the destruction had occurred out of order, then rc's destructor would have accessed an invalid reference.

Torsibility answered 23/9, 2011 at 13:48 Comment(0)
R
1

If you throw an exception inside a constructor, the object never comes to live (the zval of the object has at least a reference count of one, that's needed for the destructor), therefore there is nothing that has a destructor which could be called.

Now I wonder: How do you implement correct scope unwinding in a class hierarchy in PHP, so that subobjects are properly destroyed in the event of an exception?

In the example you give, there is nothing to unwind. But for the game, let's assume, you know that the base constructor can throw an exeception, but you need to initialize $this->foo prior calling it.

You then only need to raise the refcount of "$this" by one (temporarily), this needs (a little) more than a local variable in __construct, let's bunk this out to $foo itself:

class Der extends Base
{
  public function __construct()
  {
    parent::__construct();
    $this->foo = new Foo;
    $this->foo->__ref = $this; # <-- make base and Der __destructors active
    print("Der const.\n");
    throw new Exception("foo"); // #1
    unset($this->foo->__ref); # cleanup for prosperity
  }

Result:

Base const.
Foo const.
Der const.
Der destr.
Base destr.
Foo destr.

Demo

Think for yourself if you need this feature or not.

To control the order when the Foo destructor is called, unset the property in the destructor, like this example demonstrates.

Edit: As you can control the time when objects are constructed, you can control when objects are destructed. The following order:

Der const.
Base const.
Foo const.
Foo destr.
Base destr.
Der destr.

is done with:

class Base
{
  public function __construct() { print("Base const.\n"); }
  public function __destruct()  { print("Base destr.\n"); }
}

class Der extends Base
{
  public function __construct()
  {
    print("Der const.\n");
    parent::__construct();
    $this->foo = new Foo;
    $this->foo->__ref = $this; #  <-- make Base and Def __destructors active
    throw new Exception("foo");
    unset($this->foo->__ref);
  }
  public function __destruct()
  {
    unset($this->foo);
    parent::__destruct();
    print("Der destr.\n");
  }
  public $foo;
}

class Foo
{
  public function __construct() { print("Foo const.\n"); }
  public function __destruct()  { print("Foo destr.\n"); }
}


try {
  $x = new Der;
} catch (Exception $e) {
}
Rudich answered 28/9, 2011 at 20:30 Comment(4)
I'm not sure I buy your first sentence: If a constructor throws, member objects and base member objects may already have been initialized and may require proper destruction. In my PHP exampe, imagine that Foo has non-trivial members; and see my C++ example for how there could be a dependency of the derived class on base members.Torsibility
Also, I think your example is actually even more broken: If the constructor throws, there's no object, so the destructor should definitely not run. However, the member objects' destructors should run, followed by the base destructor. In other words, if Der::__construct() throws, the destruction sequence should be Base::const, Foo::const, Foo::dest, Base::dest.Torsibility
@kerrek SB: To the first comment: The destructor of Foo is called in your case already, Foo should take care of itself, shouldn't it? In the first of my code examples, Der destructor is called as well as Base destrcutor. If you want to change the order, you have control of that in Der destructor, I didn't change it.Rudich
@kerrek SB: To the second comment: You decide on your own if there is an object or not in PHP. As the example shows, you need to take care on your own if you want to have destructors called or not. For the order you give, I'll add a code example, it's basically that you decide on your own in which order the destructors are called if you don't like the default order (which looks fine to me).Rudich
T
1

One major difference between C++ and PHP is that in PHP, base class constructors and destructors are not called automatically. This is explicitly mentioned on the PHP Manual page for Constructors and Destructors:

Note: Parent constructors are not called implicitly if the child class defines a constructor. In order to run a parent constructor, a call to parent::__construct() within the child constructor is required.

...

Like constructors, parent destructors will not be called implicitly by the engine. In order to run a parent destructor, one would have to explicitly call parent::__destruct() in the destructor body.

PHP thus leaves the task of properly calling base class constructors and destructors entirely up to the programmer, and it is always the programmer's responsibility to call the base class constructor and destructor when necessary.

The key point in the above paragraph is when necessary. Rarely will there be a situation where failing to call a destructor will "leak a resource". Keep in mind that data members of the base instance, created when the base class constructor is called, will themselves become unreferenced, so a destructor (if one exists) for each of these members will be called. Try it out with this code:

<?php

class MyResource {
    function __destruct() {
        echo "MyResource::__destruct\n";
    }
}

class Base {
    private $res;

    function __construct() {
        $this->res = new MyResource();
    }
}

class Derived extends Base {
    function __construct() {
        parent::__construct();
        throw new Exception();
    }
}

new Derived();

Sample output:

MyResource::__destruct

Fatal error: Uncaught exception 'Exception' in /t.php:20
Stack trace:
#0 /t.php(24): Derived->__construct()
#1 {main}
  thrown in /t.php on line 20

http://codepad.org/nnLGoFk1

In this example, the Derived constructor calls the Base constructor, which creates a new MyResource instance. When Derived subsequently throws an exception in the constructor, the MyResource instance created by the Base constructor becomes unreferenced. Eventually, the MyResource destructor will be called.

One scenario where it might be necessary to call a destructor is where the destructor interacts with another system, such as a relational DBMS, cache, messaging system, etc. If a destructor must be called, then you could either encapsulate the destructor as a separate object unaffected by class hierarchies (as in the example above with MyResource) or use a catch block:

class Derived extends Base {
    function __construct() {
        parent::__construct();
        try {
            // The rest of the constructor
        } catch (Exception $ex) {
            parent::__destruct();
            throw $ex;
        }
    }

    function __destruct() {
        parent::__destruct();
    }
}

EDIT: To emulate cleaning up local variables and data members of the most derived class, you need to have a catch block to clean up each local variable or data member that is successfully initialized:

class Derived extends Base {
    private $x;
    private $y;

    function __construct() {
        parent::__construct();
        try {
            $this->x = new Foo();
            try {
                $this->y = new Bar();
                try {
                    // The rest of the constructor
                } catch (Exception $ex) {
                    $this->y = NULL;
                    throw $ex;
                }
            } catch (Exception $ex) {
                $thix->x = NULL;
                throw $ex;
            }
        } catch (Exception $ex) {
            parent::__destruct();
            throw $ex;
        }
    }

    function __destruct() {
        $this->y = NULL;
        $this->x = NULL;
        parent::__destruct();
    }
}

This is how it was done in Java, too, before Java 7's try-with-resources statement.

Tesstessa answered 29/9, 2011 at 13:21 Comment(2)
The workaround should also do any local clean-up, so that members of Derived have a chance to clean up before the base destructor is called. However, suppose the derived constructor try block contains $this->x = new Foo; $this->y = new Bar;, and both Foo's and Bar's constructor may throw, then you don't know who to clean up in the event of an exception.Torsibility
@KerrekSB: Right. That's how C++ does it. If you need that behavior in PHP, then the trick is to have a catch block for every member that is successfully constructed. See my edit.Tesstessa

© 2022 - 2024 — McMap. All rights reserved.