First the question: Why does the removal of const in UnregisterNode() cause failure, but not in RegisterNode().
Now the background: I’m wor
Your original question and the follow up in the comments to this answer all hinge on Delphi's interface reference counting mechanism.
The compiler emits code to arrange that all references to an interface are counted. Whenever you take a new reference, the count is increased. Whenever a reference is released (set to nil, goes out of scope etc.) the count is decreased. When the count reaches zero, the interface is released and in your case this is what calls Free on your objects.
Your problem is that you are cheating the reference counting by putting interface references into and out of the TList by casting to Pointer and back. Somewhere along the way the references are miscounted. I'm sure your code's behaviour (i.e. the stack overflows) could be explained but I am disinclined to attempt to do so since the code uses such obviously incorrect constructs.
Simply put you should never cast an interface to an unmanaged type like Pointer. Whenever you do so you also need to take control of the missing reference counting code. I can assure you this is something you do not want to take on!
You should use a proper type-safe container like TList or even a dynamic array and then the reference counting will be handled correctly. Making this change to your code solves the problems you describe in the question.
However, there still remains one big problem, as you have discovered for yourself and detailed in the comments.
Once you follow the reference counting rules, you are faced with the problem of circular references. In this case a node holds a reference to the container which in turn holds a reference to the node. Circular references like this cannot be broken by the standard reference counting mechanism and you have to break them yourself. Once you break one of the two individual references that make up a circular reference, the framework can do the rest.
With your current design you must break the circular references by explicitly calling UnReg on every INode that you create.
The other problem with the code as it stands is that you are using data fields of the form to hold MyContainer, MyNode etc. Because you never set MyContainer to nil then two executions of your event handler will result in a leak.
In made the following changes to your code to prove that it will run without leaking:
TContainer = class(TInterfacedObject, IContainer)
protected
NodeList: TList;//switch to type-safe list
...
procedure TContainer.RegisterNode(Node:INode);
begin
//must ensure we don't add the node twice
if NodeList.IndexOf(Node) = -1 then
NodeList.Add(Node);
end;
...
procedure TForm1.btnMakeStuffClick(Sender: TObject);
//make the interfaces local variables although in production
//code they would likely be fields and construction would happen
//in the constructor of the owning object
var
MyContainer: IContainer;
MyNode1, MyNode2, MyNode3: INode;
begin
MyContainer := TContainer.Create;
MyNode1 := TNode.Create(MyContainer);
MyNode2 := TNode.Create(MyContainer);
MyNode3 := TNode.Create(MyContainer);
MyNode1.UnReg;
MyNode1.ReReg(MyContainer);
MyNode2.UnReg;
MyNode3.UnReg;
MyNode2.ReReg(MyContainer);
MyNode1.UnReg;
MyNode2.UnReg;
end;
With these changes the code runs without memory leaks – set ReportMemoryLeaksOnShutdown := True at the start of the .dpr file to check.
It is going to be something of a bind to have to call UnReg on every node so I suggest that you simply add a method to IContainer to do that. Once you arrange that the container is capable of dropping its references then you will have a much more manageable system.
You will not be able to let reference counting do all the work for you. You will need to call IContainer.UnRegAllItems explicitly.
You can implement this new method like this:
procedure TContainer.UnRegAllItems;
begin
while NodeList.Count>0 do
NodeList[0].UnReg;
end;
Although the Delphi reference counting mechanism is very well implemented in general, there is, to my knowledge, one long-standing and very well-known bug.
procedure Foo(const I: IInterface);
begin
I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);
When Foo called in this way no code is generated to add a reference to the interface. The interface is thus released as soon as it is created and Foo acts on an invalid interface.
Because Foo receives the parameter as const, Foo does not take a reference to the interface. The bug is in the codegen for the call to Foo which mistakenly does not take a reference to the interface.
My preferred way to work around this particular problem is like this:
var
I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);
This succeeds because we explicitly take a reference.
Note that I have explained this for future reference – your current code does not fall foul of this problem.