Why, When and How to Declare / Implement Constructors, Destructor, ..., for UDTs in FB (advanced)

Forum for discussion about the documentation project.
fxm
Posts: 8020
Joined: Apr 22, 2009 12:46
Location: Paris suburb, FRANCE

Why, When and How to Declare / Implement Constructors, Destructor, ..., for UDTs in FB (advanced)

Postby fxm » Jun 11, 2018 6:35

The default constructor and the destructor are two special member procedures that are called respectively to the creation and the destruction of an object.
Any type has an implicit default constructor and an implicit destructor provided by the compiler If they are not overloaded by the user.

These constructors and destructors call the default constructors and the destructors of the base types and member data of the type, but apart from that, they do absolutely nothing.

It is therefore often necessary that user defines an explicit default constructor and an explicit destructor, in order to execute actions that must take place during the creation of an object and their destruction:
- For example, if the object contains dynamically allocated variables, it is necessary to reserve them memory when the object is created.
- At the destruction of the object, it is necessary to free the allocated memory.

Once the user defines any explicit constructor or explicit destructor, the compiler no longer automatically defines the implicit default constructor or the implicit destructor.
In particular, if the user only defines an explicit constructor taking parameters, it will no longer be possible to simply construct an object, without providing the parameters to this constructor, unless, of course, the user defines also an explicit default constructor (the one which does not take parameters).

1) Definition of constructors and destructors
    The constructor is defined as a normal member procedure. However, for the compiler to recognize it as a constructor, the following two conditions must be true:
    • It must have the same name as the type.
    • It must have no return data.
    The destructor must also respect these rules.

    A constructor is called automatically when instantiating the object. The destructor is called automatically when it is destroyed.
    This destruction occurs when outputting the current scope block for auto storage kind objects.
    For dynamically allocated objects, the constructor and destructor are automatically called by expressions that use the New, New[], and Delete, and Delete[] operators. That is why it is recommended to use them instead of the Allocate and Deallocate functions to dynamically create objects.
    Also, do not use Delete or Delete[] on 'Any Ptr' pointers, because the compiler must determine which destructor to call with the type of pointer.

    The constructor is called after the memory allocation of the object and the destructor is called before this memory is freed. The management of the dynamic allocation of memory with the types is thus simplified.
    In the case of member object fields, the order of construction is that of their declarations, and the order of destruction is the reverse. It is in this order that the constructors and destructors of each member object field are called.

    The explicit constructors may have parameters. They can be overloaded, but not the explicit destructors. This is because in general one knows the context in which an object is created, but one cannot know the context in which it is destroyed: there can only be one destructor.
    Constructors that do not take a parameter or with all parameters having a default value, automatically replace the default constructors defined by the compiler if there were no explicitly defined constructors in the types. This means that these constructors will be called automatically by the default constructors of the derived types.

  • Example - Constructors and destructor ('ZstringChain' type):

    Code: Select all

    Type ZstringChain                              '' implement a zstring chain
      Dim As Zstring Ptr pz                        '' define a pointer to the chain
      Declare Constructor ()                       '' declare the explicit default constructor
      Declare Constructor (Byval size As Integer)  '' declare the explicit constructor with as parameter the chain size
      Declare Destructor ()                        '' declare the explicit destructor
    End Type

    Constructor ZstringChain ()
      This.pz = 0  '' reset the chain pointer
    End Constructor

    Constructor ZstringChain (Byval size As Integer)
      This.pz = Callocate(size + 1, Sizeof(Zstring))  '' allocate memory for the chain
    End Constructor

    Destructor ZstringChain ()
      If This.pz <> 0 Then
        Deallocate This.pz  '' free the allocated memory if necessary
        This.pz = 0         '' reset the chain pointer
      End If
    End Destructor


    Dim As ZstringChain zc1  '' instantiate a non initialized chain : useless

    Dim As ZstringChain zc2 = ZstringChain(9)  '' instantiate a szstring chain of 9 useful characters
    '                                          '' shortcut: Dim As ZstringChain zc2 = 9
    *zc2.pz = "FreeBASIC"                      '' fill up the chain with 9 characters
    Print "zc2 chain:"
    Print "'" & *zc2.pz & "'"                  '' print the chain

    Sleep

    Code: Select all

    zc2 chain:
    'FreeBASIC'

    The constructors will sometimes have to perform more complicated tasks than those given in this example. In general, they can do all the feasible operations in a normal member procedure, except using uninitialized data of course.
    In particular, the data of inherited objects are not initialized as long as the constructors of the base types are not called. For this reason, the constructors of the base types must always be called before running the constructor of the type being instantiated.
    If the constructors of the base types are not explicitly called, the compiler will call, by default, the constructors of the base types that do not take a parameter or whose parameters have a default value (and, if no such a constructor is explicitly defined in the base types, it will call the implicit default constructors of these types).
2) Constructors and destructors when inheritance
    How to call constructors and destructors of base types when instantiating and destroying a derived type instance?
    The compiler cannot know which constructor to call among the different overloaded constructors potentially present. To call another constructor of a base type than the constructor taking no parameter, use the keyword 'Base ()' specifying the parameters to pass, and this, only authorized on the first code line of the calling constructor.

    On the other hand, it is useless to specify the destroyer to call, since this one is unique. The user must not call the destructors of the base types themselves, the compiler does it on its own by chaining the destructors.

  • Example - Explicit call of the base type constructor ('Child' type extends 'Parent' type):

    Code: Select all

    Type Parent  '' declare the parent type
      Dim As Integer I
      Declare Constructor ()
      Declare Constructor (Byval i0 As Integer)
      Declare Destructor ()
    End Type

    Constructor Parent ()  '' define parent type constructor
      Print "Parent.Constructor()"
    End Constructor

    Constructor Parent (Byval i0 As Integer)  '' define parent type constructor
      This.I = i0
      Print "Parent.Constructor(Byval As Integer)"
    End Constructor

    Destructor Parent ()  '' define parent type destructor
      Print "Parent.Destructor()"
    End Destructor

    Type Child Extends Parent  '' declare the child type
      Declare Constructor ()
      Declare Destructor ()
    End Type

    Constructor Child ()  '' define child type default constructor
      Base(123)           '' authorize only on the first code line of the constructor body
      Print "  Child.Constructor()"
    End Constructor

    Destructor Child ()  '' define child type destructor
      Print "  Child.Destructor()"
    End Destructor


    Scope
      Dim As Child c
      Print
    End Scope

    Sleep

    Code: Select all

    Parent.Constructor(Byval As Integer)
      Child.Constructor()

      Child.Destructor()
    Parent.Destructor()
      If it was not specified that the constructor to be called for the base type was the constructor taking an Integer parameter, the compiler would have called the default constructor of the base type (in the above example, one can put in comment the line 'Base(123)' and execute again to see this different behavior).
    If the type derives from several base types (multiple-level inheritance), each derived type constructor can explicitly call only one constructor, the one of its direct base type, thus the all constituting a chaining of the base constructors.

    When using subtype polymorphism (with inheritance), the object are manipulated through base type pointers or references:
    • If at least one derived type has an explicit destructor defined, all its base destructors must be virtual so that the destruction can start at this most derived type and works its way down to the last base type.
    • To do this, it may be necessary to add virtual destructors with an empty body anywhere an explicit destruction was not yet required, in order to supersede each non-virtual implicit destructor built by the compiler.
3) Copy constructors
    It will sometimes be necessary to create a copy constructor. The purpose of this kind of constructor is to initialize an object when instantiating it from another object. Any type has an implicit copy constructor automatically generated by the compiler, whose sole purpose is to copy the fields of the object to be copied one by one into the fields of the object to be instantiated. However, this implicit copy constructor will not always be enough, and the user will sometimes have to provide one explicitly.

    This will be particularly the case when some data objects have been allocated dynamically (only member pointers in the type for object aggregation). A shallow copy of the fields of one object in another would only copy the pointers, not the data pointed. Thus, changing this data for one object would result in the modification of the other object's data, which would probably not be the desired effect.

    The definition of copy constructors is like that of normal constructors. The name must be that of the type, and there must have no return data. In the parameter list, there must always be a reference on the object to be copied.

    For the 'ZstringChain' type defined above, the user needs a copy constructor.
    Its declaration is as follows:

    Code: Select all

      Declare Constructor (Byref zc As ZstringChain)  '' declare the explicit copy constructor
    and its implementation as follows:

    Code: Select all

    Constructor ZstringChain (Byref zc As ZstringChain)
      This.pz = Callocate(Len(*zc.pz) + 1, Sizeof(Zstring))  '' allocate memory for the new chain
      *This.pz = *zc.pz                                      '' initialize the new chain
    End Constructor

  • Example - Completed code with copy constructor ('ZstringChain' type):

    Code: Select all

    Type ZstringChain                                 '' implement a zstring chain
      Dim As Zstring Ptr pz                           '' define a pointer to the chain
      Declare Constructor ()                          '' declare the explicit default constructor
      Declare Constructor (Byval size As Integer)     '' declare the explicit constructor with as parameter the chain size
      Declare Constructor (Byref zc As ZstringChain)  '' declare the explicit copy constructor
      Declare Destructor ()                           '' declare the explicit destructor
    End Type

    Constructor ZstringChain ()
      This.pz = 0  '' reset the chain pointer
    End Constructor

    Constructor ZstringChain (Byval size As Integer)
      This.pz = Callocate(size + 1, Sizeof(Zstring))  '' allocate memory for the chain
    End Constructor

    Constructor ZstringChain (Byref zc As ZstringChain)
      This.pz = Callocate(Len(*zc.pz) + 1, Sizeof(Zstring))  '' allocate memory for the new chain
      *This.pz = *zc.pz                                      '' initialize the new chain
    End Constructor

    Destructor ZstringChain ()
      If This.pz <> 0 Then
        Deallocate This.pz  '' free the allocated memory if necessary
        This.pz = 0         '' reset the chain pointer
      End If
    End Destructor


    Dim As ZstringChain zc1  '' instantiate a non initialized chain : useless

    Dim As ZstringChain zc2 = ZstringChain(9)           '' instantiate a szstring chain of 9 useful characters
    '                                                   '' shortcut: Dim As ZstringChain zc2 = 9
    *zc2.pz = "FreeBASIC"                               '' fill up the chain with 9 characters
    Print "zc2 chain:"
    Print "'" & *zc2.pz & "'"                           '' print the chain
    Print
    Dim As ZstringChain zc3 = zc2                       '' instantiate a new szstring chain by copy construction
    Print "zc3 chain (zc3 copy constructed from zc2):"
    Print "'" & *zc3.pz & "'"                           '' print the chain
    Print
    *zc3.pz = "modified"                                '' modify the new chain
    Print "zc3 chain (modified):"
    Print "'" & *zc3.pz & "'"                           '' print the new chain
    Print
    Print "zc2 chain:"
    Print "'" & *zc2.pz & "'"                           '' print the copied chain (not modified)

    Sleep

    Code: Select all

    zc2 chain:
    'FreeBASIC'

    zc3 chain (zc3 copy constructed from zc2):
    'FreeBASIC'

    zc3 chain (modified):
    'modified'

    zc2 chain:
    'FreeBASIC'
4) Copy assignment operators
    For the 'ZstringChain' type defined above, the user needs also a copy assignment operator (see the 'rule of three' in a below paragraph).
    Its declaration is as follows:

    Code: Select all

      Declare Operator Let (Byref zc As ZstringChain)  '' declare the explicit copy assignment operator
    and its implementation as follows (making copy assignment safe for self-assignment):

    Code: Select all

    Operator ZstringChain.Let (Byref zc As ZstringChain)
      If @zc <> @This Then                                     '' avoid self assignment destroying the chain
        If This.pz <> 0 Then
          Deallocate This.pz                                   '' free the allocated memory if necessary
        End If
        This.pz = Callocate(Len(*zc.pz) + 1, Sizeof(Zstring))  '' allocate memory for the new chain
        *This.pz = *zc.pz                                      '' initialize the new chain
      End If
    End Operator

  • Example - Final code with also copy assignment operator ('ZstringChain' type):

    Code: Select all

    Type ZstringChain                                  '' implement a zstring chain
      Dim As Zstring Ptr pz                            '' define a pointer to the chain
      Declare Constructor ()                           '' declare the explicit default constructor
      Declare Constructor (Byval size As Integer)      '' declare the explicit constructor with as parameter the chain size
      Declare Constructor (Byref zc As ZstringChain)   '' declare the explicit copy constructor
      Declare Operator Let (Byref zc As ZstringChain)  '' declare the explicit copy assignment operator
      Declare Destructor ()                            '' declare the explicit destructor
    End Type

    Constructor ZstringChain ()
      This.pz = 0  '' reset the chain pointer
    End Constructor

    Constructor ZstringChain (Byval size As Integer)
      This.pz = Callocate(size + 1, Sizeof(Zstring))  '' allocate memory for the chain
    End Constructor

    Constructor ZstringChain (Byref zc As ZstringChain)
      This.pz = Callocate(Len(*zc.pz) + 1, Sizeof(Zstring))  '' allocate memory for the new chain
      *This.pz = *zc.pz                                      '' initialize the new chain
    End Constructor

    Operator ZstringChain.Let (Byref zc As ZstringChain)
      If @zc <> @This Then                                     '' avoid self assignment destroying the chain
        If This.pz <> 0 Then
          Deallocate This.pz                                   '' free the allocated memory if necessary
        End If
        This.pz = Callocate(Len(*zc.pz) + 1, Sizeof(Zstring))  '' allocate memory for the new chain
        *This.pz = *zc.pz                                      '' initialize the new chain
      End If
    End Operator

    Destructor ZstringChain ()
      If This.pz <> 0 Then
        Deallocate This.pz  '' free the allocated memory if necessary
        This.pz = 0         '' reset the chain pointer
      End If
    End Destructor


    Dim As ZstringChain zc1  '' instantiate a non initialized chain : useless

    Dim As ZstringChain zc2 = ZstringChain(9)           '' instantiate a szstring chain of 9 useful characters
    '                                                   '' shortcut: Dim As ZstringChain zc2 = 9
    *zc2.pz = "FreeBASIC"                               '' fill up the chain with 9 characters
    Print "zc2 chain:"
    Print "'" & *zc2.pz & "'"                           '' print the chain
    Print
    Dim As ZstringChain zc3 = zc2                       '' instantiate a new szstring chain by copy construction
    Print "zc3 chain (zc3 copy constructed from zc2):"
    Print "'" & *zc3.pz & "'"                           '' print the chain
    Print
    *zc3.pz = "modified"                                '' modify the new chain
    Print "zc3 chain (modified):"
    Print "'" & *zc3.pz & "'"                           '' print the new chain
    Print
    Print "zc2 chain:"
    Print "'" & *zc2.pz & "'"                           '' print the copied chain (not modified)
    Print
    zc3 = zc2
    Print "zc3 chain (zc3 copy assigned from zc2):"
    Print "'" & *zc3.pz & "'"                           '' print the new chain
    Print
    *zc3.pz = "changed"                                 '' modify the new chain
    Print "zc3 chain (changed):"
    Print "'" & *zc3.pz & "'"                           '' print the new chain
    Print
    Print "zc2 chain:"
    Print "'" & *zc2.pz & "'"                           '' print the copied chain (not modified)

    Sleep

    Code: Select all

    zc2 chain:
    'FreeBASIC'

    zc3 chain (zc3 copy constructed from zc2):
    'FreeBASIC'

    zc3 chain (modified):
    'modified'

    zc2 chain:
    'FreeBASIC'

    zc3 chain (zc3 copy assigned from zc2):
    'FreeBASIC'

    zc3 chain (changed):
    'changed'

    zc2 chain:
    'FreeBASIC'
5) Golden rules of good manners (for constructors, copy constructors, copy assignment operators, and destructors)
  • Reminder of behaviors impacting constructors, copy constructors, copy assignment operators, and destructors:
    • Defining an explicit default constructor replaces the implicit default constructor built by the compiler.
    • Defining an explicit constructor other than the one default suppresses the implicit default constructor built by the compiler. In this precise case, there is no default constructor at all!
    • The implicit copy constructor (or copy assignment operator, or destructor) built by the compiler can be replaced by an explicit copy constructor (or copy assignment operatoror destructor) defined by the user.
    • But (as opposed to the default constructor), there is always a copy constructor (or a copy assignment operator or a destructor), either an implicit built by the compiler or an explicit defined by the user.
    • When there is object composition, the composed object type must have an implicit or explicit constructor matching with the declaration of the compound object.
    • When there is type inheritance, the inherited type must have a default implicit or explicit constructor (unless the inheriting type has a constant-copy constructor, explicitly defined by user), and all this even if no object is constructed (compiler test on the only inheritance structure). This behavior appears to be specific to FreeBASIC.
    From all the above, one can deduce 'golden rules' that avoid most of compilation errors and run-time bugs.

  • Golden rules, for a code safer (at compile-time and run-time) but sometimes maximizing the real constraints:
    • If the user explicitly defines any constructor, he is very strongly advised to also define explicitly the default constructor as well.
    • If the user needs to explicitly define (with a non empty body) a copy constructor or a copy assignment operator or a destructor, it is better to define the 3 simultaneously (the known 'rule of three'), plus the default constructor (rule above also applied).
6) Member access rights impact (on declaration of constructors, copy constructors, copy assignment operators, and destructors)
    Access rights can be applied when declaring such member procedures, to prohibit the user from performing certain specific commands (from the outside of type).

    The default constructor, copy constructor, copy assignment operator, and destructor are the only member procedures which have an implicit version built by the compiler.
    So if one want to forbid their accesses by the user from the outside of the type, it is necessary to overload these by explicit versions with restricted access rights (not Public) when declaring. In addition, such member procedures may have no implementation (no body defining) if they are never actually called in the program.

  • Example - Inheritance structure where any base object construction must be forbidden:
    • In order to forbid any construction of base objet from the outside of types, the default constructor and the copy constructor of base type must be explicitly declared (to overload their implicit versions) with restricted access rights.
    • The base type default constructor cannot be declared as Private, because it must be accessible from the derived type (to construct a derived object), thus it is declared as Protected. It must have an implementation
    • The base type copy constructor can be declared as Private because it is never called in this example. So it may have no implementation.

      Code: Select all

      Type Parent
        Public:
          Dim As Integer I
        Protected:
          Declare Constructor ()
        Private:
          Declare Constructor (Byref p As Parent)
      End Type

      Constructor Parent
      End Constructor

      Type Child Extends Parent
        Public:
          Dim As Integer J
      End Type

      Dim As Child c1
      Dim As Child C2 = c1
      c2 = c1

      'Dim As Parent p1                        '' forbidden
      'Dim As Parent p2 = c1                   '' forbidden
      'Dim As Parent Ptr pp1 = New Parent      '' forbidden
      'Dim As Parent Ptr pp2 = New Parent(c1)  '' forbidden

      Sleep
  • Example - Singleton structure (at most one object can exist at any time):
    • The singleton construction must only be done by calling the static procedure 'Singleton.create()'.
    • So, the default constructor and the copy constructor must be explicitly declared (to overload their implicit versions) with restricted access rights as Private, in order to forbid any object user creation by 'Dim' or 'New'. Only the copy constructor may have no implementation because it will never be called (the default constructor is called from inside the type by 'New').
    • The singleton destruction must only be done by calling the static procedure 'Singleton.suppress()'.
    • So, the destructor must be explicitly declared (to overload its implicit version) with restricted access rights as Private, in order to forbid any object user destruction by 'Delete' (the destructor must have implementation because it is called from inside the type by 'Delete').

      Code: Select all

      Type Singleton
        Public:
          Declare Static Function create () As Singleton Ptr
          Declare Static Sub suppress ()
          Dim i As Integer
        Private:
          Static As Singleton Ptr ref
          Declare Constructor ()
          Declare Constructor (Byref rhs As Singleton)
          Declare Destructor ()
      End Type

      Dim As Singleton Ptr Singleton.ref = 0

      Static Function Singleton.create () As Singleton Ptr
        If Singleton.ref = 0 Then
          Singleton.ref = New Singleton
          Return Singleton.ref
        Else
          Return 0
        End If
      End Function

      Static Sub Singleton.suppress ()
        If Singleton.ref > 0 Then
          Delete Singleton.ref
          Singleton.ref = 0
        End If
      End Sub

      Constructor Singleton ()
      End Constructor

      Destructor Singleton ()
      End Destructor


      Dim As Singleton Ptr ps1 = Singleton.create()
      ps1->i = 1234
      Print ps1, ps1->i

      Dim As Singleton Ptr ps2 = Singleton.create()
      Print ps2

      Singleton.suppress()

      Dim As Singleton Ptr ps3 = Singleton.create()
      Print ps3, ps3->i

      Singleton.suppress()

      'Delete ps3                                      '' forbidden
      'Dim As Singleton s1                             '' forbidden
      'Dim As Singleton s2 = *ps3                      '' forbidden
      'Dim As Singleton Ptr ps4 = New Singleton        '' forbiden
      'Dim As Singleton Ptr ps5 = New Singleton(*ps3)  '' forbidden

      Sleep

      Code: Select all

      5122656        1234
      0
      5122656        0
See also:
- How and Why to Define Constructors, Assignment-Operators, and Destructor, for UDTs in FB
- How and Why to make Abstraction by Object Encapsulation, with FB Syntax in UDTs (basics)
- How and Why to make Abstraction by Subtype Polymorphism, with FB Syntax in UDTs (advanced)

Return to “Documentation”

Who is online

Users browsing this forum: No registered users and 1 guest