[RFC] Magic Method that handles unset($object)

php.internals

Sebastian Bergmann

19 years ago
Consider the following code: <?php class ParentC { public $children = array(); public function __construct() { print "ParentC::__construct()\n"; } public function __destruct() { print "ParentC::__destruct()\n"; } public function addChild( $child ) { $child->parentO = $this; $this->children[] = $child; } } class Child { public $parentO = null; public function __construct() { print "Child::__construct()\n"; } public function __destruct() { print "Child::__destruct()\n"; } } function setup() { $parent = new ParentC; $child = new Child; $parent->addChild( $child ); return $parent; } while( true ) { $parent = setup(); unset( $parent ); } ?> This code prints ParentC::__construct() Child::__construct() ParentC::__construct() Child::__construct() . . . instead of ParentC::__construct() Child::__construct() Child::__destruct() ParentC::__destruct() ParentC::__construct() Child::__construct() Child::__destruct() ParentC::__destruct() . . . because unset($parent) decrements the refcount from 2 to 1 as the $child object that is aggregated by the $parent object still holds a reference to its aggregating object. The infinite while() loop is used to hide the fact that the objects are destructed at interpreter shutdown. While this works, it is not desirable in the context from which this small reproducible script is extracted. A possible solution to this problem would be the introduction of yet another "magic" method. This method would be automatically called when unset() is called on an object that implements it. Unfortunately, the name __unset() is already taken. Assuming we had such a method, the following version of ParentC would behave as desired: <?php class ParentC { public $children = array(); public function __construct() { print "ParentC::__construct()\n"; } public function __destruct() { print "ParentC::__destruct()\n"; } public function __new_magic_method() { $this->children = array(); } public function addChild( $child ) { $child->parentO = $this; $this->children[] = $child; } } ?> Is there another solution to this problem (apart from explicitly unsetting the $children array on the $parent object)?And even if there is I think that this method would be usefull.
-- Sebastian Bergmann http://sebastian-bergmann.de/ GnuPG Key: 0xB85B5D69 / 27A7 2B14 09E4 98CD 6277 0E5B 6867 C514 B85B 5D69

Etienne Kneuss

19 years ago
Hi, Sebastian Bergmann wrote:
> > Is there another solution to this problem (apart from explicitly > unsetting the $children array on the $parent object)?And even if there > is I think that this method would be usefull. >
You could use references, you would have to move some code around, though. I'm not sure that's exactly what you want, but give it a shot: class ParentC { public $children = array(); public function __construct() { print "ParentC::__construct()\n"; } public function __destruct() { print "ParentC::__destruct()\n"; } } class Child { public $parentO = null; public function __construct() { print "Child::__construct()\n"; } public function __destruct() { print "Child::__destruct()\n"; } } function &setup() { $parent = new ParentC; $child = new Child; $parent->children[] = &$child; $child->parentO = &$parent; return $parent; } $parent = &setup(); $parent = null; echo "--end--\n"; Result: ParentC::__construct() Child::__construct() ParentC::__destruct() Child::__destruct() --end--
-- Etienne Kneuss http://www.colder.ch colder@php.net Men never do evil so completely and cheerfully as when they do it from a religious conviction. -- Pascal

Sebastian Bergmann

19 years ago
Etienne Kneuss wrote:
> ParentC::__construct() > Child::__construct() > ParentC::__destruct() > Child::__destruct()
You only get this result (ie. the __destruct() calls) because you reach the end of the script, not because unset() is called.
-- Sebastian Bergmann http://sebastian-bergmann.de/ GnuPG Key: 0xB85B5D69 / 27A7 2B14 09E4 98CD 6277 0E5B 6867 C514 B85B 5D69

Etienne Kneuss

19 years ago
Hi, nah, take a look at my echo "--end--\n"; which gets displayed after the __destruct calls. The $parent = null will trigger the destructors. Sebastian Bergmann wrote:
> Etienne Kneuss wrote: > >> ParentC::__construct() >> Child::__construct() >> ParentC::__destruct() >> Child::__destruct() >> > > You only get this result (ie. the __destruct() calls) because you reach > the end of the script, not because unset() is called. > >
-- Etienne Kneuss http://www.colder.ch colder@php.net Men never do evil so completely and cheerfully as when they do it from a religious conviction. -- Pascal

Sara Golemon

19 years ago
> A possible solution to this problem would be the introduction of yet > another "magic" method. This method would be automatically called when > unset() is called on an object that implements it. Unfortunately, the > name __unset() is already taken. > > Is there another solution to this problem (apart from explicitly > unsetting the $children array on the $parent object)?And even if there > is I think that this method would be usefull. >
To do this >right< you'd need notification, not just that unset() was called on the object, but what the current state of *both* of its relevant reference counts are. class foo { public function __delref($objectstore_refcount, $zval_refcount) { if ($objectstore_refcount == 1) { /* Only one zval is pointing to this object, * e.g. the one used/shared by the child's backreference */ if ($zval_refcount == 1) { /* Only one variable is pointing to this zval, * the child's backreference property */ $this->children = array(); } } } } Of course, that assumes you only have one child. Once you introduce multiple children, they'll *probably share one object store reference through a common zval with multiple references, so you *could* do: class foo { public function __delref($objectstore_refcount, $zval_refcount) { if ($objectstore_refcount == 1) { /* Only one zval is pointing to this object, * e.g. the one used/shared by the child's backreference */ if ($zval_refcount == count($this->children)) { /* Only one children's properties are pointing to this zval */ $this->children = array(); } } } } But that doesn't cover the (quite common) case where there are multiple object store references spread out among the children (and possibly other variables not contained in the object itself). Unfortunately you *can't* aggregate the total number of variable->zval->object references ince at this point in runtime, only the zval being dereferenced is known for certain. By the way, this is part of the reason why PHP doesn't have delete(). -Sara

Sebastian Bergmann

19 years ago
Sara Golemon wrote:
> To do this >right< you'd need notification, not just that unset() was > called on the object, but what the current state of *both* of its > relevant reference counts are.
I fail to see why. If we introduce a method that is called for unset() then there should be no problem with resetting the $children attribute to array() in my example: unset($parent) -> "magic method" -> $children = array() -> no references to $child objects Nothing would happen in the case that you describe where other references to the $child objects exist, of course.
-- Sebastian Bergmann http://sebastian-bergmann.de/ GnuPG Key: 0xB85B5D69 / 27A7 2B14 09E4 98CD 6277 0E5B 6867 C514 B85B 5D69

Ants Aasma

19 years ago
Sebastian Bergmann wrote:
> Sara Golemon wrote: >> To do this >right< you'd need notification, not just that unset() was >> called on the object, but what the current state of *both* of its >> relevant reference counts are. > > I fail to see why. If we introduce a method that is called for > unset() then there should be no problem with resetting the > $children attribute to array() in my example:
In what way is this different from defining method close() and instead of unset($object) call $object->close()? What happens if you have more than one non-child reference to parent?
-- Ants Aasma

Arnold Daniels

19 years ago
Hi, I tried to bring up this subject a view days ago, but without much success. Very good that it's brought up again. Meanwhile I thought about the problem myself. Another way to solve this problem is to look for circular references upon unset. This might be bad for performance, but I believe that preventing a memory leak is something that is the responsibility of PHP, not of the user. ---- The unset() statement could do something like the following (pseudo code): void unset(object) { object->refcount--; list = new Linked_List(); // Linked list with counter for each item count_references(list, object); foreach (list->items as item) { if (item->count != item->object->refcount) return; } foreach (list->items as item) { destroy(item->object); } } void count_references(list, object) { if (list->has_object(object)) { list->inc_count(object); return; } list->add(object); foreach (object->children as child) { count_references(list, child); } } ----- It's important that when an object is destroyed because refcount is 0, unset() is called for all it's children. Also when a variable is unset because of leaving the call stack, unset() should be used. When destoy() is called, it is not necessary to call unset() this. ----- Imagine the following situation: $x --> A --------> B --> C <--> D $y -| | ^ | ->F <-> G | |-> E <-- $z | | -------------| $% is a reference within global or static space or within the call stack. //--- Example 1 --- unset($x); // Would simply lower the refcount of A unset($y); // Would destroy A, F and G, but not the rest unset($z); // Would destroy the rest (B, C, D and E) //--- Example 2 --- unset($z); // Would simply lower the refcount of E unset($x); // Would simply lower the refcount of A unset($y); // Would destroy everything ----- I don't think writing a magic unset method will do the trick, because would cause a problem if you would do: $a = new MyClass(); $b = $a; unset($b); Also when refcount is lowered because of leaving the call stack, calling unset would cause a problem. Not calling unset would still cause the memory leak. I've I'm correct, the method should work in all situations. Best regards, Arnold Daniels Javeline - Create what's next. http://www.javeline.net -----Original Message----- From: news [mailto:news@sea.gmane.org] On Behalf Of Sebastian Bergmann Sent: zondag 10 december 2006 17:26 To: internals@lists.php.net Subject: Re: [PHP-DEV] Re: [RFC] Magic Method that handles unset($object) Sara Golemon wrote:
> To do this >right< you'd need notification, not just that unset() was > called on the object, but what the current state of *both* of its > relevant reference counts are.
I fail to see why. If we introduce a method that is called for unset() then there should be no problem with resetting the $children attribute to array() in my example: unset($parent) -> "magic method" -> $children = array() -> no references to $child objects Nothing would happen in the case that you describe where other references to the $child objects exist, of course.
-- Sebastian Bergmann http://sebastian-bergmann.de/ GnuPG Key: 0xB85B5D69 / 27A7 2B14 09E4 98CD 6277 0E5B 6867 C514 B85B 5D69 -- PHP Internals - PHP Runtime Development Mailing List To unsubscribe, visit: http://www.php.net/unsub.php

Ants Aasma

19 years ago
Arnold Daniels wrote:
> Another way to solve this problem is to look for circular references upon > unset. This might be bad for performance, but I believe that preventing a > memory leak is something that is the responsibility of PHP, not of the user.
That would indeed be too slow. The standard way to handle this, is to only check visibility periodically. That is how Java, .NET, Python and countless others do that. Check http://www.hpl.hp.com/personal/Hans_Boehm/gc/ for a sample of how an implementation might look like. Ants