Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scope unwinding in PHP class constructors

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).

like image 436
Kerrek SB Avatar asked Sep 19 '11 12:09

Kerrek SB


People also ask

What is __ construct in PHP?

PHP - The __construct FunctionA constructor allows you to initialize an object's properties upon creation of the object. If you create a __construct() function, PHP will automatically call this function when you create an object from a class. Notice that the construct function starts with two underscores (__)!

What is difference between constructor and destructor in PHP?

Constructor is called automatically, while the object is created. Destructor is called automatically, as block is exited or program terminates. Constructor allows an object to initialize some of its value before, it is used. Destructor allows an object to execute some code at the time of its destruction.


2 Answers

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.

like image 107
NikiC Avatar answered Oct 09 '22 19:10

NikiC


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.

like image 32
Kerrek SB Avatar answered Oct 09 '22 19:10

Kerrek SB