Terminating Dependent Classes

disco chick
I have four objects that are all dependent on each other. They are CInvoice has many CInvoiceLines, CInvoiceLine has many CRoyaltyLines, CRoyaltyLine has many CRoyaltyAdjustments. In order to properly terminate all of these class modules, I need to remove any and all dependencies. If I don’t, the classes will stay alive and consume memory. With four levels like this eating memory, I will eventually have to answer the question: Do I want to send an error report?

I think I have my termination sequences right, but I’ll let you be the judge in the comments. I start in the CInvoice class, which I’ll call a Parent class. It has many children, including the CInvoiceLines class (a child collection) and each individual CInvoiceLine class (a child class). The CInvoiceLine class then becomes a parent class for my next round of terminations. His children are the CRoyaltyLines class and each instance of the CRoyaltyLine class.

All of my Terminate events are structured like

Public Sub Terminate()
 
End Sub

I don’t use the built-in class terminate event because it doesn’t fire at the right time. When I’m in a parent class, I do these three things:

  1. Call the Terminate method of the Child Collection Class
  2. Set the local Child Collection Class variable to Nothing
  3. Set the local Parent variable to Nothing

Items 1 and 2 are done in a parent class that is not also a child class. Item 3 is done in a child class that is not also a parent class. All three items are done in a class that is both a parent and a child. For instance, CInoviceLine is a parent with respect to CRoyaltyLines, but a child with respect to CInvoice, so all three steps must occur.

When I’m in a Child Collection Class (ex: CInvoiceLines), I do these three things:

  1. Call the Terminate method for each member of the collection
  2. Set the local Parent variable to Nothing
  3. Set the local Collection variable to Nothing

When item 1 is executed, it may be terminating a class that’s a parent class and the whole things starts over again. Here’s an example: In the CInvoice class

Public Sub Terminate()
   
    mobjLines.Terminate  ‘Term the child collection class
   Set mobjLines = Nothing ‘term the local ccc variable

End Sub

Since mobjLines is a CInvoiceLines object, that Terminate method gets called first. In the CInvoiceLines class

Public Sub Terminate()
   
    Dim i As Long
   
    ‘Terminate each member
   For i = 1 To mcolLines.Count
        mcolLines.Item(i).Terminate
    Next i
       
    Set mobjParent = Nothing ‘kill the parent variable
   Set mcolLines = Nothing ‘kill the collection variable
   
End Sub

When mcolLines.Item(i).Terminate is called, it’s calling the Terminate method of a class that’s both a child and a parent. In CInvoiceLine

Public Sub Terminate()
   
    mobjRoyaltyLines.Terminate ‘Term the child collection class
   Set mobjParent = Nothing    ‘kill the parent variable
   Set mobjRoyaltyLines = Nothing   ‘kill the ccc variable
   
End Sub

The only difference between this and CInvoice is that I killed the parent variable because it’s also a child class. I won’t go through the rest, except to show you the last class, CRoyaltyAdjustment

Public Sub Terminate()
   
    Set mobjParent = Nothing
   
End Sub

Since this is a child class, but not a parent class, only killing the local parent variable is necessary.

Boy, there’s nothing more thrilling than terminating classes, is there? That’s why I added the image – to spice it up a little. For a little background, I’m abstracting my relational database into objects in VBA. That way I can reference clsInvoice.Lines(1).RoyaltyLines(2).Adjustments(3).Amount, which I contend is easier to code and read. But the setup is a real pain.

Posted in Uncategorized

10 thoughts on “Terminating Dependent Classes

  1. Dick,

    As far as I know there has never been a need to clean up memory with VB(A). Of course, I could be wrong about the historical need but there is absolutely no need to do any cleanup in todays environment. I suspect that is true for all languages except for unmanaged C++.

    The Garbage Collector will do the needful.

    And, I definitely don’t believe you should be creating and calling a public Terminate method. Terminate is a *private* method that is automatically invoked by VB.

    In the following test, I nested an object of type Class2 in an object of type Class3. Each Class2 object had 500 nested object of type Class1, which in turn contained a string variable initialized with a string 1,000,000 characters long.

    There was no cleanup code.

    Memory usage on my PC went from about 766MB to about 1.77GB. I checked this when the VBA code reached the Stop statement. Then, with the next step, which terminated execution of the testMemory routine, memory usage dropped back to 766MB.

    I ran the above test thrice. Very similar results each time.

    ‘Class1
    Option Explicit
    Dim aStr As String
    Private Sub Class_Initialize()
        aStr = String(1000000, “a”)
        End Sub

    ‘Class2
    Option Explicit
    Dim anObj() As Class1
    Private Sub Class_Initialize()
        ReDim anObj(499)
        Dim I As Integer
        For I = LBound(anObj) To UBound(anObj)
            Set anObj(I) = New Class1
            Next I
        End Sub

    ‘Class3
    Option Explicit

    Dim anObj2 As Class2

    Private Sub Class_Initialize()
        Set anObj2 = New Class2
        End Sub

    ‘Module1
    Option Explicit

    Sub testMemory()
        Dim x As Class3
        Set x = New Class3
        Stop
        End Sub

  2. My 2 cents:
    >>As far as I know there has never been a need to clean up memory with VB(A)….The Garbage Collector will do the needful.

    ?, Surely not Tushar, even in .Net you stil need to call the GC, (often twice!) to control when it runs, if you leave it to the framework, you can be sure when it will run and clear your “dead” objects from memory.

    In VBA I thought it was good practice to set all objects to Nothing when you have finished with them, thus removing them from the memory, but are you saying that VBA classes don’t need this? If the code keeps running then the memory will not be released? will it?
    Interesting stuff…
    I ran your code Tushar, my results where somewhat different. Looking at the task manager mem usage for Excel.Exe:
    opened excel = ~40’000 k
    run code = ~200’000 k
    code stops = ~60’000 k

    But there was large difference in the numbers.

    look forward to seeing what other have to say.
    Cheers
    Ross

  3. Tushar: If you put a Parent property in Class1 and set it equal to Class2, I think you will get different results. I agree as long as the dependency only goes one way, VB cleans itself up well. Make these changes

    ‘Class1
    Public Parent As Class2
     
    ‘Class2
    Private Sub Class_Initialize()
        ReDim anObj(499)
        Dim I As Integer
        For I = LBound(anObj) To UBound(anObj)
            Set anObj(I) = New Class1
            Set anObj(I).Parent = Me
            Next I
        End Sub

    The first blip is the original, the second is never ending.

  4. Unless your Terminate methods are referring to global public original instances of the parent objects, I suspect your memory leak is akin to the difference between the two numbers in the last line of output from

    Option Explicit

    Sub test()
      Dim c1 As New Collection, c2 As Collection
      Set c2 = c1
      Debug.Print countof(c1), countof(c2)
      c1.Add 1, “a”
      c1.Add 2, “b”
      Debug.Print countof(c1), countof(c2)
      c2.Add 3, “c”
      Debug.Print countof(c1), countof(c2)
      Set c2 = Nothing
      Debug.Print countof(c1), countof(c2)
    End Sub

    Function countof(x As Object) As Long
      On Error Resume Next
      countof = x.Count
    End Function

    That is, if your variables representing objects in your Terminate calls are just REFERENCES to those objects rather than the original objects themselves, setting them to nothing only frees the reference, not the original object.

  5. Your Parent properties are causing circular references. These don’t get cleared until the original process is reset. In effect, that’s a memory leak.

    The VB workaround is an absolutely beautiful hack first demonstrated, I believe, by Bruce McKinney. First, change your internal variable for the parent object to a long. This is going to point to the object rather than refer to it. Then use the undocumented objptr and the hack objfromptr functions to convert the object to and from its pointer. This way you’re never storing an object reference, so the circular reference in not created.

    ‘ In your child class
    Private m_lngParentPtr As Long
    Private Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” _
                                   (dest As Any, Source As Any, ByVal bytes As Long)

    ‘ The Parent property
    Public Property Get Parent() As Class1
        Set Parent = ObjFromPtr(m_lngParentPtr)
    End Property
    Public Property Set Parent(obj As Class1)
        m_lngParentPtr = ObjPtr(obj)
    End Property

    ‘Returns an object given its pointer.
    ‘This function reverses the effect of the ObjPtr function.
    Private Function ObjFromPtr(ByVal pObj As Long) As Object
        Dim obj                     As Object
        ‘ force the value of the pointer into the temporary object variable
        CopyMemory obj, pObj, 4
        ‘ assign to the result (this increments the ref counter)
        Set ObjFromPtr = obj
        ‘ manually destroy the temporary object variable
        ‘ (if you omit this step you’ll get a GPF!)
        CopyMemory obj, 0&, 4
    End Function

    HTH

    Rob

  6. Dick: Yes, you are absolutely correct. Implementing a parent property requires some additional work to release the memory. I went back and looked at some data structures with two-way object references that I’d implemented over the past few years. While that code still looks good, I will have to remember the issue raised by this discussion.

    Rob’s post about the hack should prove valuable for those implementing a two-way object reference. I assume it would work in the example I shared in my first post in this discussion.

  7. Excellent, thanks for the solution. Key missing command in my code was the 2nd CopyMemory to destory the temporary memory (I thought that Set obj = Nothing was good enough). My error would occur after running the CopyMemory and then exiting Excel. 5 – 10 secs later, it would give me a “Microsoft Excel has stopped working” error message.

Leave a Reply

Your email address will not be published. Required fields are marked *