Instantiating objects that allocate memory

General FreeBASIC programming questions.
Post Reply
GWBASICcoder
Posts: 21
Joined: Jul 29, 2022 12:43

Instantiating objects that allocate memory

Post by GWBASICcoder »

Hi,
I have a fairly simple example that is showing

Code: Select all

free(): double free detected in tcache 2

error that aborts with a core dump.

I am instantiating the objects like this:

Code: Select all

'---- main
dim dt as dataobj
dt = dataobj("54")
print "Press key to end ..."
sleep
Am I doing anything incorrect? If I instantiate like this:

Code: Select all

dim as dataobj dt = dataobj("54")
there is no problem, but I am curious why the first method does not work.

The full code is:

Code: Select all

const null = 0

type dataobj
	private:
		datstring as zstring ptr
		declare sub destroy ()
	public:
		declare constructor ()
		declare constructor (k as string)
		declare destructor ()
end type

sub dataobj.destroy()
	'deallocate datstring if present and set ptr to null.
	if this.datstring <> null then
		deallocate this.datstring
		this.datstring = null
	end if
end sub

constructor dataobj ()
	'print "constructor dataobj no params"
	datstring = null
end constructor

'constructor for key datstring
constructor dataobj (k as string)
	destroy
	dim as integer l = len(k)
	If l > 0 then
	   	datstring = allocate(l + 1)
	   	*datstring = k
	else
	   	datstring = null
	end if
end constructor

destructor dataobj ()
	destroy
end destructor

'---- main
dim dt as dataobj
dt = dataobj("54")
print "Press key to end ..."
sleep
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

1)
'dim as dataobj dt = dataobj("54")' constructs an initialized dataobj instance in one go.

2)
'dim dt as dataobj' constructs a (uninitialized) dataobj instance.
'dt = dataobj("54")' constructs a temporary initialized instance (with 'dataobj("54")'), copies this temporary instance into the first instance (with 'dt = '), then destroys the temporary instance.
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

For the second case, you must also define a copy-operator (operator 'let') with data allocation for the zstring character data, otherwise by default, only the pointer value will be copied, and therefroe the same zstring character data will be destroyed two times at the end:

Code: Select all

const null = 0

type dataobj
	private:
		datstring as zstring ptr
		declare sub destroy ()
	public:
		declare constructor ()
		declare constructor (k as string)
		declare destructor ()
        declare operator let (d as dataobj)
end type

sub dataobj.destroy()
	'deallocate datstring if present and set ptr to null.
	if this.datstring <> null then
		deallocate this.datstring
		this.datstring = null
	end if
end sub

constructor dataobj ()
	'print "constructor dataobj no params"
	datstring = null
end constructor

'constructor for key datstring
constructor dataobj (k as string)
	destroy
	dim as integer l = len(k)
	If l > 0 then
	   	datstring = callocate(l + 1)  '' advised for zstring character data
	   	*datstring = k
	else
	   	datstring = null
	end if
end constructor

destructor dataobj ()
	destroy
end destructor

operator dataobj.let (d as dataobj)
    if @d <> @this then                   '' avoid self assignment destroying the chain
        destroy
        dim as integer l = len(*d.datstring)
        If l > 0 then
            datstring = callocate(l + 1)  '' advised for zstring character data
            *datstring = *d.datstring
        else
            datstring = null
        end if
    end if
end operator

'---- main
dim dt as dataobj
dt = dataobj("54")
print "Press key to end ..."
sleep

Note:
If you wanted to use this following case as well:
'dim as dataobj dt1 = dataobj("54")'
'dim as dataobj dt2 = dt1'

You would then also have to define a copy-constructor:
'declare constructor (d as dataobj)'
Last edited by fxm on Aug 08, 2022 18:47, edited 1 time in total.
Reason: Added test in operator 'let' body to avoid self-assignment.
GWBASICcoder
Posts: 21
Joined: Jul 29, 2022 12:43

Re: Instantiating objects that allocate memory

Post by GWBASICcoder »

Got it! It worked!

Thanks very much.

fxm wrote: Aug 08, 2022 8:50 For the second case, you must also define a copy-operator (operator 'let') with data allocation for the zstring character data, otherwise by default, only the pointer value will be copied, and therefroe the same zstring character data will be destroyed two times at the end:

Code: Select all

const null = 0

type dataobj
	private:
		datstring as zstring ptr
		declare sub destroy ()
	public:
		declare constructor ()
		declare constructor (k as string)
		declare destructor ()
        declare operator let (d as dataobj)
end type

sub dataobj.destroy()
	'deallocate datstring if present and set ptr to null.
	if this.datstring <> null then
		deallocate this.datstring
		this.datstring = null
	end if
end sub

constructor dataobj ()
	'print "constructor dataobj no params"
	datstring = null
end constructor

'constructor for key datstring
constructor dataobj (k as string)
	destroy
	dim as integer l = len(k)
	If l > 0 then
	   	datstring = callocate(l + 1)  '' advised for zstring character data
	   	*datstring = k
	else
	   	datstring = null
	end if
end constructor

destructor dataobj ()
	destroy
end destructor

operator dataobj.let (d as dataobj)
    destroy
	dim as integer l = len(*d.datstring)
	If l > 0 then
	   	datstring = callocate(l + 1)  '' advised for zstring character data
	   	*datstring = *d.datstring
	else
	   	datstring = null
	end if
end operator

'---- main
dim dt as dataobj
dt = dataobj("54")
print "Press key to end ..."
sleep

Note:
If you wanted to use this following case as well:
'dim as dataobj dt1 = dataobj("54")'
'dim as dataobj dt2 = dt1'

You would then also have to define a copy-constructor:
'declare constructor (d as dataobj)'
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

To implement the copy-constructor, the code is very simple because it suffices to call (implicitly) the copy-operator (operator 'let') previously implemented:

Code: Select all

constructor dataobj (d as dataobj)
    this = d
end constructor

Note: For such a type, defining a copy-constructor is mandatory not only for object copy-construction but also when such an object is passed by value to a procedure, and when a function returns such an object by value by using the 'Return' keyword, because the copy-constructor is also called in these 2 cases.
Last edited by fxm on Aug 10, 2022 6:26, edited 2 times in total.
Reason: Added note.
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

For safer code of the copy-operator (operator 'let'), we must avoid self-assignment which would destroy the zstring outright:

Code: Select all

operator dataobj.let (d as dataobj)
    if @d <> @this then                   '' avoid self assignment destroying the chain
        destroy
        dim as integer l = len(*d.datstring)
        If l > 0 then
            datstring = callocate(l + 1)  '' advised for zstring character data
            *datstring = *d.datstring
        else
            datstring = null
        end if
    end if
end operator

Note: For such a type, defining a copy-operator (operator 'let') is mandatory not only for object assignment but also when a function returns such an object by value by using the 'Function=' keyword, because the copy-operator is also called in this case.
Last edited by fxm on Aug 10, 2022 6:27, edited 2 times in total.
Reason: Added note.
dodicat
Posts: 7976
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Instantiating objects that allocate memory

Post by dodicat »

"Lazy" fb (as opposed to pure fb)

Code: Select all

#cmdline "-exx"  'optional

redim shared memory() as string
#macro allocatememory(S,slot)
  redim preserve memory(ubound(memory)+1)
  memory(ubound(memory))=S
  slot=strptr(memory(ubound(memory)))
#endmacro


type dataobj
	'private:
		datstring as zstring ptr
		declare sub destroy ()
           
	public:
		declare constructor ()
		declare constructor (k as  string)
		declare destructor ()
end type




sub dataobj.destroy()

	if this.datstring <> 0 then
        'nothing to do   
      
	end if
end sub

constructor dataobj ()
	datstring = 0
end constructor

'constructor for key datstring
constructor dataobj (k as  string) 
	'destroy
     
	dim as integer l = len(k)
	If l > 0 then
           allocatememory(string(l,0),datstring)
           *datstring = k 
          
	else
	   	datstring = 0
	end if
end constructor

destructor dataobj ()
	'destroy what?
end destructor

'---- main

redim as dataobj d(100000)
for n as long=0 to ubound(d)
  d(n)=  dataobj(str(n)+chr(32,45,32)+str(100000-n)) 
next

for n as long=0 to 20
      print *d(n).datstring
next

print "..."
for n as long=ubound(d)-20 to ubound(d)
      print *d(n).datstring
next
print
print "Press a key"

sleep
erase memory

 
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

From the initial proposed code (1st post), a complete example of a Type that explicitly allocates memory from zstring data (through a pointer as member), with all member procedures for constructions, assignments, value output, and destruction:
- 3 constructors (default construction, construction from zstring variable, copy-construction),
- 2 operators 'Let' (assignment from zstring variable, copy-assignmenr),
- 1 operator 'Cast' (value output),
- 1 destructor (destruction).

Code: Select all

type dataobj
    public:
        declare constructor ()
        declare constructor (byref as const zstring)
        declare constructor (byref as const dataobj)
        declare operator let (byref as const zstring)
        declare operator let (byref as const dataobj)
        declare operator cast () byref as const zstring
        declare destructor ()
    private:
        datstring as zstring ptr
        declare sub destroy ()
end type

constructor dataobj ()
    destroy
end constructor

constructor dataobj (byref k as const zstring)
    this = k  '' using 'operator let (byref as zstring)'
end constructor

constructor dataobj (byref d as const dataobj)
    this = d  '' using 'operator let (byref as dataobj)'
end constructor

operator dataobj.let (byref k as const zstring)
    destroy
    dim as integer l = len(k)
    If l > 0 then
        datstring = callocate(l + 1)  '' advised for zstring character data
        *datstring = k
    else
        datstring = 0
    end if
end operator

operator dataobj.let (byref d as const dataobj)
    if @d <> @this then                   '' avoid self assignment destroying the chain
        destroy
        dim as integer l = len(*d.datstring)
        If l > 0 then
            datstring = callocate(l + 1)  '' advised for zstring character data
            *datstring = *d.datstring
        else
            datstring = 0
        end if
    end if
end operator

operator dataobj.cast () byref as const zstring
    return *datstring
end operator

destructor dataobj ()
    destroy
end destructor

sub dataobj.destroy()
    if this.datstring <> 0 then
        deallocate this.datstring
        this.datstring = 0
    end if
end sub

'---- main

scope
    
    dim dt1 as dataobj = dataobj("54")  '' using 'constructor (byref as zstring)'
    print dt1                           '' using 'operator cast () byref as const zstring'

    dim dt2 as dataobj                  '' using 'constructor ()'
    dt2 = dataobj("54")                 '' using 'constructor (byref as zstring)' + 'operator let (byref as dataobj)'
    print dt2                           '' using 'operator cast () byref as const zstring'

    dim dt3 as dataobj = dataobj(dt1)   '' using 'constructor (byref as dataobj)'
    print dt3                           '' using 'operator cast () byref as const zstring'

    dim dt4 as dataobj                  '' using 'constructor ()'
    dt4 = dt1                           '' using 'operator let (byref as dataobj)'
    print dt4                           '' using 'operator cast () byref as const zstring'

    dim dt5 as dataobj                  '' using 'constructor ()'
    dt5 = "54"                          '' using 'operator let (byref as zstring)'
    print dt5                           '' using 'operator cast () byref as const zstring'

end scope                               '' using 'destructor ()'

sleep
dodicat
Posts: 7976
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Instantiating objects that allocate memory

Post by dodicat »

It does the job perfectly, but it is not obvious.

Code: Select all

'By fxm
type dataobj
    public:
        declare constructor ()
        declare constructor (byref as const zstring)
        declare constructor (byref as const dataobj)
        declare operator let (byref as const zstring)
        declare operator let (byref as const dataobj)
        declare operator cast () byref as const zstring
        declare destructor ()
    private:
        datstring as zstring ptr
        declare sub destroy ()
end type
dim shared as long make,break

constructor dataobj ()
    destroy
end constructor

constructor dataobj (byref k as const zstring)
    this = k  '' using 'operator let (byref as zstring)'
end constructor

constructor dataobj (byref d as const dataobj)
    this = d  '' using 'operator let (byref as dataobj)'
end constructor

operator dataobj.let (byref k as const zstring)
    destroy
    dim as integer l = len(k)
    If l > 0 then
          make+=1
        datstring = callocate(l + 1)  '' advised for zstring character data
        *datstring = k
    else
        datstring = 0
    end if
end operator

operator dataobj.let (byref d as const dataobj)
    if @d <> @this then                   '' avoid self assignment destroying the chain
        destroy
        dim as integer l = len(*d.datstring)
        If l > 0 then
              make+=1
            datstring = callocate(l + 1)  '' advised for zstring character data
            *datstring = *d.datstring
        else
            datstring = 0
        end if
    end if
end operator

operator dataobj.cast () byref as const zstring
    return *datstring
end operator

destructor dataobj ()
    destroy
end destructor

sub dataobj.destroy()
    if this.datstring <> 0 then
          break+=1
        deallocate this.datstring
        this.datstring = 0
    end if
end sub

'---- main


do
redim as dataobj d(100000)
for n as long=0 to ubound(d)
  d(n)=  dataobj(str(n)+chr(32,45,32)+str(100000-n)) 
next
locate 1,1,0
for n as long=0 to 10
      print d(n)
next

print "..."
for n as long=ubound(d)-10 to ubound(d)
      print d(n)
next

print "create","destroy","difference"
print make,break,make-break
print

print "Press a key to end"
if len(inkey) then exit do

loop

end

sub finish destructor
      print "final (wait 5 seconds)" 
      print "create","destroy","difference"
      print make,break,make-break
      sleep 5000
      end sub

 
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

fxm wrote: Aug 13, 2022 16:26 From the initial proposed code (1st post), a complete example of a Type that explicitly allocates memory from zstring data (through a pointer as member), with all member procedures for constructions, assignments, value output, and destruction:
- 3 constructors (default construction, construction from zstring variable, copy-construction),
- 2 operators 'Let' (assignment from zstring variable, copy-assignmenr),
- 1 operator 'Cast' (value output),
- 1 destructor (destruction).

Code: Select all

type dataobj
    public:
        declare constructor ()
        declare constructor (byref as const zstring)
        declare constructor (byref as const dataobj)
        declare operator let (byref as const zstring)
        declare operator let (byref as const dataobj)
        declare operator cast () byref as const zstring
        declare destructor ()
    private:
        datstring as zstring ptr
        declare sub destroy ()
end type

constructor dataobj ()
    destroy
end constructor

constructor dataobj (byref k as const zstring)
    this = k  '' using 'operator let (byref as zstring)'
end constructor

constructor dataobj (byref d as const dataobj)
    this = d  '' using 'operator let (byref as dataobj)'
end constructor

operator dataobj.let (byref k as const zstring)
    destroy
    dim as integer l = len(k)
    If l > 0 then
        datstring = callocate(l + 1)  '' advised for zstring character data
        *datstring = k
    else
        datstring = 0
    end if
end operator

operator dataobj.let (byref d as const dataobj)
    if @d <> @this then                   '' avoid self assignment destroying the chain
        destroy
        dim as integer l = len(*d.datstring)
        If l > 0 then
            datstring = callocate(l + 1)  '' advised for zstring character data
            *datstring = *d.datstring
        else
            datstring = 0
        end if
    end if
end operator

operator dataobj.cast () byref as const zstring
    return *datstring
end operator

destructor dataobj ()
    destroy
end destructor

sub dataobj.destroy()
    if this.datstring <> 0 then
        deallocate this.datstring
        this.datstring = 0
    end if
end sub

'---- main

scope
    
    dim dt1 as dataobj = dataobj("54")  '' using 'constructor (byref as zstring)'
    print dt1                           '' using 'operator cast () byref as const zstring'

    dim dt2 as dataobj                  '' using 'constructor ()'
    dt2 = dataobj("54")                 '' using 'constructor (byref as zstring)' + 'operator let (byref as dataobj)'
    print dt2                           '' using 'operator cast () byref as const zstring'

    dim dt3 as dataobj = dataobj(dt1)   '' using 'constructor (byref as dataobj)'
    print dt3                           '' using 'operator cast () byref as const zstring'

    dim dt4 as dataobj                  '' using 'constructor ()'
    dt4 = dt1                           '' using 'operator let (byref as dataobj)'
    print dt4                           '' using 'operator cast () byref as const zstring'

    dim dt5 as dataobj                  '' using 'constructor ()'
    dt5 = "54"                          '' using 'operator let (byref as zstring)'
    print dt5                           '' using 'operator cast () byref as const zstring'

end scope                               '' using 'destructor ()'

sleep

A variant for the 'operator let (byref as const dataobj)' which can simply call the 'operator let (byref as const zstring)' after the test for no self assignment.
So, only the 'operator let (byref as const zstring)' body contains the code of memory allocation (the other three call it indirectly).

Code: Select all

operator dataobj.let (byref d as const dataobj)
    if @d <> @this then      '' avoid self assignment destroying the chain
        this = *d.datstring  '' using 'operator let (byref as const zstring)'
    end if
end operator
GWBASICcoder
Posts: 21
Joined: Jul 29, 2022 12:43

Re: Instantiating objects that allocate memory

Post by GWBASICcoder »

Hi all,

Thanks for the replies. Newbie question here: why is passing d as dataobj as a reference required?

Code: Select all

operator dataobj.let (byref d as const dataobj)
    if @d <> @this then      '' avoid self assignment destroying the chain
        this = *d.datstring  '' using 'operator let (byref as const zstring)'
    end if
end operator
I have

Code: Select all

operator dataobj.let (d as dataobj)
	if @this = @d then
		exit operator
	end if
...
and it seems to work fine.
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

Extract of the BYVAL / BYREF (parameters) documentation page:
In the -lang fb dialect, ByVal is the default parameter passing convention for all built-in types except String and user-defined Type which are passed ByRef by default. The ZString and WString built-in types are also passed ByRef by default, but passing ByVal is forbidden. Arrays are always passed ByRef and the use of the specifier ByRef or ByVal is forbidden.
So, even when 'Byref' is omitted, a UDT instance is always passed by reference.
We can pass a UDT instance by value by specifying 'Byval'.

But in case of cloning operator (like the copy-constructor and the Let operator defined for copy-assignment) which constructs or assigns from another existing UDT instance, passing by reference is mandatory.
(I think this last requirement is not specified in the documentation => to be updated)
This is a known explanation for the copy-constructor because "byval arg" itself generally calls the copy-constructor, and this would induce an infinite loop.
For the let operator, there are cases where "byval arg" is coded as default-construction + assignment, when for example the UDT has no copy-constructor but has an object field with a default-constructor or a Base with default-constructor.


[edit]
Documentation update done:
- KeyPgConstructor → fxm [a copy constructor can not take into account its argument (the object to clone) by value]
- KeyPgOpLet → fxm [when the operator Let (assign) is defined for copy assignment, its parameter (the object to clone) can not be passed by value]
- ProPgCtorsAssignDtors → fxm [a copy constructor and a copy-assignment operator can not take into account their argument (the object to clone) by value]
Last edited by fxm on Aug 15, 2022 21:10, edited 6 times in total.
dodicat
Posts: 7976
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Instantiating objects that allocate memory

Post by dodicat »

From the help file:
"ByRef is better for passing huge objects like strings or big UDTs that should not be copied"

A warning is given when passing "big" UDT's, it looks like "big" is when:
64 bit compiler, > 512 bytes
32 bit compiler, > 256 bytes
(in the case of an array anyway)

Code: Select all

#ifdef __FB_64BIT__ 
#define sz 513
#else
#define sz 257
#endif

type udt 
      as byte d(1 to sz)
end type


sub dothis(byval u as udt)
      u.d(ubound(u.d))=50
      print u.d(ubound(u.d))
      print sizeof(udt)
end sub

dim as udt u
dothis(u)
sleep 
fxm
Moderator
Posts: 12081
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: Instantiating objects that allocate memory

Post by fxm »

fxm wrote: Aug 15, 2022 8:50 ... in case of cloning operator (like the copy-constructor and the Let operator defined for copy-assignment) which constructs or assigns from another existing UDT instance, passing by reference is mandatory.
This is a known explanation for the copy-constructor because "byval arg" itself generally calls the copy-constructor, and this would induce an infinite loop.
For the let operator, there are cases where "byval arg" is coded as default-construction + assignment, when for example the UDT has no copy-constructor but has an object field with a default-constructor or a Base with default-constructor.

- In this following case, It can be checked that 'Byval u As UDT' calls the explicit copy-constructor (added by the user):

Code: Select all

Type UDT
    Dim As Integer I
    Declare Constructor()
    Declare Constructor(Byref u As UDT)
End Type

Constructor UDT()
End Constructor

Constructor UDT(Byref u As UDT)
    Print "copy-constructor called"
    This.I = u.I
End Constructor

Sub test(Byval u As UDT)
End Sub

Dim As UDT u

Print "Passing a UDT instance by value to a procedure:"
test(u)

Sleep
Passing a UDT instance by value to a procedure:
copy-constructor called

- In this following case, 'Byval u As UDT' calls the implicit copy-constructor (added by the compiler):

Code: Select all

Type UDT
    Dim As Integer I
    Declare Constructor()
    Declare Operator Let(Byref u As UDT)
End Type

Constructor UDT()
End Constructor

Operator UDT.Let(Byref u As UDT)
    Print "copy-assignment operator called"
    This.I = u.I
End Operator

Sub test(Byval u As UDT)
End Sub

Dim As UDT u

Print "Passing a UDT instance by value to a procedure:"
test(u)

Sleep
Passing a UDT instance by value to a procedure:

- In this following case (more complex UDT data field with an implicit default-constructor), It can be checked that 'Byval u As UDT' calls the explicit copy-assignment operator (added by the user):

Code: Select all

Type UDT
    Dim As String S
    Declare Constructor()
    Declare Operator Let(Byref u As UDT)
End Type

Constructor UDT()
End Constructor

Operator UDT.Let(Byref u As UDT)
    Print "copy-assignment operator called"
    This.S = u.S
End Operator

Sub test(Byval u As UDT)
End Sub

Dim As UDT u

Print "Passing a UDT instance by value to a procedure:"
test(u)

Sleep
Passing a UDT instance by value to a procedure:
copy-assignment operator called
Post Reply