Object lifetime with reference counting

Post your FreeBASIC source, examples, tips and tricks here. Please don’t post code without including an explanation.
Post Reply
coderJeff
Site Admin
Posts: 4308
Joined: Nov 04, 2005 14:23
Location: Ontario, Canada
Contact:

Object lifetime with reference counting

Post by coderJeff »

This following code demonstrates:
1) Reference Counting for objects
2) Separating Interface from Implementation

By separating the interface from the data, then passing around the object either by value or by reference doesn't really matter, under the hood its all by reference. No more costly than passing pointers, although there is some overhead for incrementing/decrementing the reference count. That's the price to pay for letting your program manage the lifetime of the object.

I have placed many comments in the code to explain what each step is doing. The demo code is can be compiled and run, with the intent that you can examine the output alongside the program.

Code: Select all

'' ========================================================
'' Reference Counting Demo
'' 
'' This demo puts all the files in one listing only to make
'' compiling and testing easier.  Normally, I would put
'' each part in different headers and sources for reasons
'' which are explained below.
''
'' ========================================================
'' REFCOUNT.BI
'' ========================================================

'' REFERENCE COUNTING HELPER

'' the REFCOUNTER class tracks number of references on
'' another object.  It's code is reusable so it gets it's
'' own header/source
''
'' The REFCOUNTER object is a counter.  It's implemented
'' as a class/object so it can get initialized like an object
'' when used as a member of another TYPE created with
'' DIM or NEW.

#ifndef __REFCOUNT_BI_INCLUDE__
#define __REFCOUNT_BI_INCLUDE__

''
type REFCOUNTER
	declare constructor()
	declare destructor()
	declare function IncRef( byval p as any ptr ) as integer
	declare function DecRef( byval p as any ptr ) as integer
	refcount as integer
end type

#endif

'' ========================================================
'' REFCOUNT.BAS
'' ========================================================

'' REFERENCE COUNTING HELPER

'' !!! #include once "refcount.bi"

'' Implentation of the reference counter - doesn't do much
'' other than provide an interface.  Conceptually though,
'' an object, when created will have a reference count of
'' one (1), and as the object is assigned to other variables
'' parameters, etc, the reference count will increase.
'' As the references go out of scope, the reference count
'' will decrease by one (1), each time until the reference
'' count is zero (0), at which point the acutal object
'' data is deleted (destroyed).

''
constructor REFCOUNTER()
	refcount = 0
end constructor

''
destructor REFCOUNTER()
end destructor

''
function REFCOUNTER.IncRef( byval p as any ptr ) as integer
	'' the 'p' argument is optional, but could be used
	'' for degugging, memory leak detection, etc
	refcount += 1
	print "    IncRef(@" & hex(p) & ")"
	function = refcount
end function

''
function REFCOUNTER.DecRef( byval p as any ptr ) as integer
	'' the 'p' argument is optional, but could be used
	'' for degugging, memory leak detection, etc
	print "    DecRef(@" & hex(p) & ")"
	if( refcount ) then
		refcount -= 1
	end if
	function = refcount
end function

'' ========================================================
'' MYOBJECT.BI
'' ========================================================

'' MYOBJECT INTERFACE

#ifndef __MYOBJECT_BI_INCLUDE__
#define __MYOBJECT_BI_INCLUDE__

'' This "object" holds only a reference (pointer) to the 
'' actual object data.  When assigning this object to a
'' variable or passing it byval or byref as a parameter
'' we are really only passing a reference (a pointer
'' reference that is).  This object is more like an
'' interface to an object with some extra code to
'' handle reference counting than an independant object.

'' the actual data is in MYOBJECT_CTX, but we are making
'' it really private, besides, it is not declared yet,
'' so just provide a forward alias.
type MYOBJECT_CTX_ as MYOBJECT_CTX

type MYOBJECT
	
	private:
		'' pointer to the actual object data - this
		'' (pointer) reference is the only data
		'' stored in the interface.
		  
		ctx as MYOBJECT_CTX_ ptr

	public:

		'' Minimally, need to implement the following:
		'' constructor to allow: 
		''     DIM x as MYOBJECT
		'' copy constructor to allow: 
		''     DIM y as MYOBJECT = x
		'' assignment operator to allow: 
		''     x = y
		'' destructor:
		''     clean-up when MYOBJECT goes out of scope

		declare constructor()
		declare constructor( byref rhs as const MYOBJECT )
		declare destructor()
		declare operator let ( byref rhs as const MYOBJECT )

		declare property text() as string
		declare property text( byref value as const string )

end type

#endif

'' ========================================================
'' MYOBJECT.BAS
'' ========================================================

'' MYOBJECT IMPLEMENTATION

'' This is the implementation of the MYOBJECT interface
'' and a module-private definition of the data that the
'' interface makes use of.

'' !!! #include once "refcount.bi"
'' !!! #include once "myobject.bi"

const NULL = cast(any ptr, 0)

'' --------------------------------------------------------
'' MYOBJECT_CTX - DATA
'' --------------------------------------------------------

'' MYOBJECT_CTX is the actual object data
type MYOBJECT_CTX
	refc as REFCOUNTER
	text as string

	'' ctor/dtor are optional, only for demo purposes
	declare constructor()
	declare destructor()
end type

''
constructor MYOBJECT_CTX()
	print "    new MYOBJECT_CTX() = data @" & hex(@this)
end constructor

''
destructor MYOBJECT_CTX()
	print "    delete MYOBJECT_CTX() = data @" & hex(@this)
end destructor

'' --------------------------------------------------------
'' MYOBJECT - INTERFACE
'' --------------------------------------------------------

''
constructor MYOBJECT()
	'' We are constructing an object and it is going to
	'' be refenced by either a variable, parameter,
	'' temporary type, so it needs a reference
	'' count of at least one (1).
	''
	'' For Example: DIM x AS MYOBJECT


	ctx = new MYOBJECT_CTX
	print "    new MYOBJECT() ref @" & @this & " => data @" & hex(ctx)
	if( ctx <> NULL ) then
		ctx->refc.IncRef( ctx )
	end if

end constructor

''
constructor MYOBJECT( byref rhs as const MYOBJECT )
	'' We are constructing an object by
	'' making a copy of an existing object
	'' Keep in mind though, we are actually just
	'' constructing a reference to the real object
	'' data.  So, just copy the refence to the already
	'' created object and increment the reference
	'' count on it.

	ctx = rhs.ctx
	print "    new MYOBJECT() ref @" & @this & " => data @" & hex(ctx)
	if( ctx <> NULL ) then
		ctx->refc.IncRef( ctx )
	end if
end constructor

''
operator MYOBJECT.let( byref rhs as const MYOBJECT )

	'' We are assigning a MYOBJECT variable
	'' to another MYOBJECT variable - we need to
	'' decrement the reference counter on one, 
	'' and increment the reference counter on the
	'' other.

	'' Don't do anything if THIS object and
	'' RHS both reference the same MYOBJECT_CTX

	if( ctx <> rhs.ctx ) then

		print "    assignment MYOBJECT() ref @" & @this & " => data @" & hex(rhs.ctx) & " (was @" & hex(ctx) & ")"

		'' DECREMENT REFERENCE COUNTER
		'' if THIS object reference is initialized,
		'' then decrement it's reference count because
		'' we are about to assign a reference to some
		'' other object (of the same type)

		if( ctx <> NULL ) then

			'' decrement the reference count and test
			'' if the reference count is zero

			if( ctx->refc.DecRef( ctx ) = 0 ) then

				'' reference count is zero, so, it is
				'' OK to actually release the object
				delete ctx

			end if
		end if

		'' ASSIGNMENT - actually we are just copying
		'' references.  We have already decremented
		'' the reference count for the expression on the
		'' left hand side of the expression.

		ctx = rhs.ctx

		'' INCREMENT REFERENCE COUNTER (on valid reference only)

		if( ctx <> NULL ) then
			ctx->refc.IncRef( ctx )
		end if

	end if

end operator

''
destructor MYOBJECT()
	'' Keep in mind that this is the destructor
	'' for the reference object only, the real
	'' object data is in ctx as MYOBJECT_CTX ptr.
	'' So, decrement the reference counter and only
	'' actually delete the object data if the 
	'' reference count gets to zero (0).
	
	print "    delete MYOBJECT() ref @" & @this & " => data @" & hex(ctx)
		
	if( ctx ) then
		if( ctx->refc.DecRef( ctx ) = 0 ) then
			delete ctx
			ctx = NULL
		end if
	end if
end destructor

''
property MYOBJECT.text() as string
	property = ctx->text
end property

''
property MYOBJECT.text( byref value as const string )
	ctx->text = value
end property

'' ========================================================
'' DEMO.BAS
'' ========================================================

'' !!! #include once "myobject.bi"

print "Begin"
scope 
	'' !!! #include once "myobject.bi"

	'' create an reference 'a' to a new MYOBJECT object
	'' using default constructor
	print "  dim a as MYOBJECT"
	dim a as MYOBJECT

	print "Begin B+C scope"
	scope
		

		'' create a reference 'b' that is same object as 'a'
		'' using copy constuctor
		print "  dim b as MYOBJECT = a"
		dim b as MYOBJECT = a

		'' create a reference 'c' to a new MYOBJECT object
		'' using default constructor
		print "  dim c as MYOBJECT"
		dim c as MYOBJECT

		'' now 'c' is also same object as 'a'
		'' using let operator
		print "  c = a"
		c = a

		'' setting text in one place is seen by all references
		c.text = "Only one data object"

		print "B:" & b.text
		print "C:" & c.text

		'' 'b' and 'c' go out of scope, the references are removed
		'' and the reference count is decreased on the actual object
		'' but it is not yet removed because 'a' still holds a 
		'' reference to the data.
	
	end scope
	print "End B+C scope"

	print "A:" & a.text

end scope
print "End"
fxm
Moderator
Posts: 12066
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Object lifetime with reference counting

Post by fxm »

(from viewtopic.php?p=274815#p274815)
coderJeff wrote:Can you please critique Object lifetime with reference counting?
What bothers me is the data member 'text' and especially the data member 'refc' which are in the actual object data 'MYOBJECT_CTX'.
I would prefer to add a second pointer (to 'REFCOUNTER') in 'MYOBJECT' (and embed 'text' in 'REFCOUNTER' for example), and be forced to manage dynamic allocations / deallocations in parallel with regard to the 'MYOBJECT_CTX' and 'REFCOUNTER' instances.

For example, quick modification without touching the comment:

Code: Select all

'' ========================================================
'' Reference Counting Demo
''
'' This demo puts all the files in one listing only to make
'' compiling and testing easier.  Normally, I would put
'' each part in different headers and sources for reasons
'' which are explained below.
''
'' ========================================================
'' REFCOUNT.BI
'' ========================================================

'' REFERENCE COUNTING HELPER

'' the REFCOUNTER class tracks number of references on
'' another object.  It's code is reusable so it gets it's
'' own header/source
''
'' The REFCOUNTER object is a counter.  It's implemented
'' as a class/object so it can get initialized like an object
'' when used as a member of another TYPE created with
'' DIM or NEW.

#ifndef __REFCOUNT_BI_INCLUDE__
#define __REFCOUNT_BI_INCLUDE__

''
type REFCOUNTER
   public:
      declare constructor()
      declare destructor()
      declare function IncRef( byval p as any ptr ) as integer
      declare function DecRef( byval p as any ptr ) as integer
      declare property TextRef() as string
      declare property TextRef( byref value as const string )
   private:
      refcount as integer
      text as string
end type

#endif

'' ========================================================
'' REFCOUNT.BAS
'' ========================================================

'' REFERENCE COUNTING HELPER

'' !!! #include once "refcount.bi"

'' Implentation of the reference counter - doesn't do much
'' other than provide an interface.  Conceptually though,
'' an object, when created will have a reference count of
'' one (1), and as the object is assigned to other variables
'' parameters, etc, the reference count will increase.
'' As the references go out of scope, the reference count
'' will decrease by one (1), each time until the reference
'' count is zero (0), at which point the acutal object
'' data is deleted (destroyed).

''
constructor REFCOUNTER()
   refcount = 0
end constructor

''
destructor REFCOUNTER()
end destructor

''
function REFCOUNTER.IncRef( byval p as any ptr ) as integer
   '' the 'p' argument is optional, but could be used
   '' for degugging, memory leak detection, etc
   refcount += 1
   print "    IncRef(@" & hex(p) & ")"
   function = refcount
end function

''
function REFCOUNTER.DecRef( byval p as any ptr ) as integer
   '' the 'p' argument is optional, but could be used
   '' for degugging, memory leak detection, etc
   print "    DecRef(@" & hex(p) & ")"
   if( refcount ) then
      refcount -= 1
   end if
   function = refcount
end function

''
property REFCOUNTER.TextRef() as string
   property = text
end property

''
property REFCOUNTER.TextRef( byref value as const string )
   text = value
end property

'' ========================================================
'' MYOBJECT.BI
'' ========================================================

'' MYOBJECT INTERFACE

#ifndef __MYOBJECT_BI_INCLUDE__
#define __MYOBJECT_BI_INCLUDE__

'' This "object" holds only a reference (pointer) to the
'' actual object data.  When assigning this object to a
'' variable or passing it byval or byref as a parameter
'' we are really only passing a reference (a pointer
'' reference that is).  This object is more like an
'' interface to an object with some extra code to
'' handle reference counting than an independant object.

'' the actual data is in MYOBJECT_CTX, but we are making
'' it really private, besides, it is not declared yet,
'' so just provide a forward alias.
type MYOBJECT_CTX_ as MYOBJECT_CTX

type MYOBJECT
   
   private:
      '' pointer to the actual object data - this
      '' (pointer) reference is the only data
      '' stored in the interface.
       
      ctx as MYOBJECT_CTX_ ptr
      refc as REFCOUNTER ptr

   public:

      '' Minimally, need to implement the following:
      '' constructor to allow:
      ''     DIM x as MYOBJECT
      '' copy constructor to allow:
      ''     DIM y as MYOBJECT = x
      '' assignment operator to allow:
      ''     x = y
      '' destructor:
      ''     clean-up when MYOBJECT goes out of scope

      declare constructor()
      declare constructor( byref rhs as const MYOBJECT )
      declare destructor()
      declare operator let ( byref rhs as const MYOBJECT )

      declare property text() as string
      declare property text( byref value as const string )

end type

#endif

'' ========================================================
'' MYOBJECT.BAS
'' ========================================================

'' MYOBJECT IMPLEMENTATION

'' This is the implementation of the MYOBJECT interface
'' and a module-private definition of the data that the
'' interface makes use of.

'' !!! #include once "refcount.bi"
'' !!! #include once "myobject.bi"

const NULL = cast(any ptr, 0)

'' --------------------------------------------------------
'' MYOBJECT_CTX - DATA
'' --------------------------------------------------------

'' MYOBJECT_CTX is the actual object data
type MYOBJECT_CTX

   '' ctor/dtor are optional, only for demo purposes
   declare constructor()
   declare destructor()
   __ as integer
end type

''
constructor MYOBJECT_CTX()
   print "    new MYOBJECT_CTX() = data @" & hex(@this)
end constructor

''
destructor MYOBJECT_CTX()
   print "    delete MYOBJECT_CTX() = data @" & hex(@this)
end destructor

'' --------------------------------------------------------
'' MYOBJECT - INTERFACE
'' --------------------------------------------------------

''
constructor MYOBJECT()
   '' We are constructing an object and it is going to
   '' be refenced by either a variable, parameter,
   '' temporary type, so it needs a reference
   '' count of at least one (1).
   ''
   '' For Example: DIM x AS MYOBJECT


   ctx = new MYOBJECT_CTX
   refc = new REFCOUNTER
   print "    new MYOBJECT() ref @" & @this & " => data @" & hex(ctx)
   if( ctx <> NULL and refc <> NULL) then
      refc->IncRef( ctx )
   else
      if( ctx <> NULL ) then
         delete ctx
         ctx = 0
      end if
      if( refc <> NULL ) then
         delete refc
         refc = 0
      end if
   end if

end constructor

''
constructor MYOBJECT( byref rhs as const MYOBJECT )
   '' We are constructing an object by
   '' making a copy of an existing object
   '' Keep in mind though, we are actually just
   '' constructing a reference to the real object
   '' data.  So, just copy the refence to the already
   '' created object and increment the reference
   '' count on it.

   ctx = rhs.ctx
   refc = rhs.refc
   print "    new MYOBJECT() ref @" & @this & " => data @" & hex(ctx)
   if( ctx <> NULL ) then
      refc->IncRef( ctx )
   end if
end constructor

''
operator MYOBJECT.let( byref rhs as const MYOBJECT )

   '' We are assigning a MYOBJECT variable
   '' to another MYOBJECT variable - we need to
   '' decrement the reference counter on one,
   '' and increment the reference counter on the
   '' other.

   '' Don't do anything if THIS object and
   '' RHS both reference the same MYOBJECT_CTX

   if( ctx <> rhs.ctx ) then

      print "    assignment MYOBJECT() ref @" & @this & " => data @" & hex(rhs.ctx) & " (was @" & hex(ctx) & ")"

      '' DECREMENT REFERENCE COUNTER
      '' if THIS object reference is initialized,
      '' then decrement it's reference count because
      '' we are about to assign a reference to some
      '' other object (of the same type)

      if( ctx <> NULL ) then

         '' decrement the reference count and test
         '' if the reference count is zero

         if( refc->DecRef( ctx ) = 0 ) then

            '' reference count is zero, so, it is
            '' OK to actually release the object
            delete ctx
            delete refc

         end if
      end if

      '' ASSIGNMENT - actually we are just copying
      '' references.  We have already decremented
      '' the reference count for the expression on the
      '' left hand side of the expression.

      ctx = rhs.ctx
      refc = rhs.refc

      '' INCREMENT REFERENCE COUNTER (on valid reference only)

      if( ctx <> NULL ) then
         refc->IncRef( ctx )
      end if

   end if

end operator

''
destructor MYOBJECT()
   '' Keep in mind that this is the destructor
   '' for the reference object only, the real
   '' object data is in ctx as MYOBJECT_CTX ptr.
   '' So, decrement the reference counter and only
   '' actually delete the object data if the
   '' reference count gets to zero (0).
   
   print "    delete MYOBJECT() ref @" & @this & " => data @" & hex(ctx)
      
   if( ctx <> NULL ) then
      if( refc->DecRef( ctx ) = 0 ) then
         delete ctx
         ctx = NULL
         delete refc
         refc = NULL
      end if
   end if
end destructor

''
property MYOBJECT.text() as string
   property = refc->TextRef
end property

''
property MYOBJECT.text( byref value as const string )
   refc->TextRef = value
end property

'' ========================================================
'' DEMO.BAS
'' ========================================================

'' !!! #include once "myobject.bi"

print "Begin"
scope
   '' !!! #include once "myobject.bi"

   '' create an reference 'a' to a new MYOBJECT object
   '' using default constructor
   print "  dim a as MYOBJECT"
   dim a as MYOBJECT

   print "Begin B+C scope"
   scope
      

      '' create a reference 'b' that is same object as 'a'
      '' using copy constuctor
      print "  dim b as MYOBJECT = a"
      dim b as MYOBJECT = a

      '' create a reference 'c' to a new MYOBJECT object
      '' using default constructor
      print "  dim c as MYOBJECT"
      dim c as MYOBJECT

      '' now 'c' is also same object as 'a'
      '' using let operator
      print "  c = a"
      c = a

      '' setting text in one place is seen by all references
      c.text = "Only one data object"

      print "B:" & b.text
      print "C:" & c.text

      '' 'b' and 'c' go out of scope, the references are removed
      '' and the reference count is decreased on the actual object
      '' but it is not yet removed because 'a' still holds a
      '' reference to the data.
   
   end scope
   print "End B+C scope"

   print "A:" & a.text

end scope
print "End"
MrSwiss
Posts: 3910
Joined: Jun 02, 2013 9:27
Location: Switzerland

Re: Object lifetime with reference counting

Post by MrSwiss »

fxm wrote:I would prefer to add a second pointer (to 'REFCOUNTER') in 'MYOBJECT' (and embed 'text' in 'REFCOUNTER' for example), ...
No, sorry, disagree totally.
The reason is (as far as I understand coderJeff's intention) that, the (hirarchical) dependency,
should only be "downwards" and not, both ways (which the proposal intends to do).

The hirarchy as I see it is:
REFCOUNTER -- level 0
followed by -- level 1:
MYOBJECT as well as MYOBJECT_CTX (which I'd rename as: MYOBJECT_DATA) to more clearly
show the relatonship between the two (should be seen, as 'educational' change, only).

level 0 is currently independent of all else (and, should stay that way).
fxm's proposal whould, in fact, destroy the 'independence' of REFCOUNTER.
(MYOBJECT may use anyhing else than String, too. This, without impact on REFCOUNTER.)
coderJeff
Site Admin
Posts: 4308
Joined: Nov 04, 2005 14:23
Location: Ontario, Canada
Contact:

Re: Object lifetime with reference counting

Post by coderJeff »

fxm, yeah, I think maybe we are expecting different goals, and I know I have not been clear now reading my original post. I think MrSwiss pretty much gets the idea of it.

The idea that with:

Code: Select all

type T
	private:
		ctx as T_ctx ptr
	public:
		declare ....
end type
That's all. So the only thing that is copied / passed is a pointer.

Reading back on my opening post, I realize I missed some information. The general intent is that 'T_ctx' object is very private and only exposed through T.

The 'REFCOUNTER' is not much more that an actual counter. It could easily be a couple of '#define's'

This pattern has worked quite well for me, but it tends to fall apart with derived types.

Let me get back with a better example, as I can see now I have not given enough info in this tip/trick.
fxm
Moderator
Posts: 12066
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Object lifetime with reference counting

Post by fxm »

I started with the idea that the aim was a utility to manage user objects by the way of "smart" references that could even be multiple on the same instance:
- More precisely I thought MYOBJECT_CTX was the user type structure for which MYOBJECT (with RFCOUNTER) provided a user interface to help him handle a collection of "smart" references to his instantiated objects (potentially multiple references possible to the same MYOBJECT_CTX instance).
- I thought finally to complete MYOBJECT with operators like 'CAST', '->', '*', as for a "smart" pointer.
Post Reply