I'm wondering what I've tripped over here:
type AObj = object
name: string
var a: AObj
a.name = "abc" # This compiles fine
a.name = "def" # So does this
# proc `=destroy`(x: AObj) = # This declaration triggers a compile error "'x.name' cannot be assigned to" in the body
proc `=destroy`(x: var AObj) = # This declaration triggers a deprecation warning, but body compiles fine
x.name = ""
Hooks must be declared before first use. If I am not wrong the correct way of doing this would be:
type AObj = object
name: string
proc `=wasMoved`(x: var AObj) =
x.name = ""
proc `=destroy`(x: AObj) =
`=destroy`(x.name)
var a: AObj
a.name = "abc"
a.name = "def"
I guess my example wasn't the best.
My point is that an object's fields are not allowed to be mutated in =destroy - my code example still fails to compile if the 3 lines starting with var a are removed.
Explicitly calling the destructor for a field, as your example shows, works fine if said field is not a resource that is shared with other objects. But there would be problems if it is shared.
My main concern was with the case where the same ref object is shared among several parent objects. Setting the field to nil in the destructor would have the side effect of decrementing shared object's reference count, and hence delay destroying the ref object until the last parent object is destroyed.
I had thought that, if =destroy for the shared object was explicitly called in the parent object's destructor, then the shared object's destructor would be called multiple times - once for each parent object's destruction. This would obviously be a problem.
But that turns out not to be the case, as the code example below shows. It appears that calling =destroy on an object does not actually destroy it if the reference count is greater than 1.
BTW, I also found out that explicitly decrementing the shared object's reference count in the parent's destructor by calling GC_unref also works. The two different calls seem to have the same effect.
type AObj = object
name: string
proc `=destroy`(x: AObj) =
echo "Destroying A named ", x.name
proc newAObj(name: string): ref AObj =
result = AObj.new()
result.name = name
type BObj = object
aref: ref AObj
name: string
proc `=destroy`(x: BObj) =
echo "Destroying B named ", x.name
# Commenting out **both** statements below causes a leak - the destructor
# for aref is never called
`=destroy`(x.aref) # Explicit destructor call - this works fine
# GC_unref(x.aref) # Explicit reference count decrement - this also works fine
proc newBObj(name: string, aref: ref AObj): ref BObj =
result = BObj.new()
result.aref = aref
result.name = name
echo "Creating A"
var a = newAObj("A#1")
echo "Creating B#1"
var bobj1 = newBObj("B#1", a)
echo "Creating B#2"
var bobj2 = newBObj("B#2", a)
echo "Setting A outside of parents to NIL"
a = nil
echo "Setting B#1 to NIL"
bobj1 = nil
echo "Setting B#2 to NIL"
bobj2 = nil
echo "... Done"
The correct way to do it is this:
type BObj = object
aref: ref AObj
name: string
proc `=destroy`(x: BObj) =
echo "Destroying B named ", x.name
`=destroy`(x.aref)
`=destroy`(x.name)
Strings must be destroyed too.
Good to know. Thanks!
A couple of other details from my experiments:
Using =destroy only works if the proc is defined for the shared object type (of course). But it does work for non "ref T" things like string (as you mention above) and seq.
Using GC-unref does not require =destroy to be defined for the type. So it could be used for objects that are shared among parents but don't have any non-numeric fields, and hence don't need a destructor. But it does not work for things like string and seq.
We need to improve the documentation: https://nim-lang.org/docs/destructors.html#lifetimeminustracking-hooks-nimeqdestroy-hook
Unfortunately it's not ok to tinker with these things until they work as you might trigger implementation defined behavior which means that the behavior might change in the future.