How and Why to Define Constructors, Assignment-Operators, and Destructor, for UDTs in FB

Forum for discussion about the documentation project.
fxm
Posts: 8687
Joined: Apr 22, 2009 12:46
Location: Paris (suburbs), FRANCE

How and Why to Define Constructors, Assignment-Operators, and Destructor, for UDTs in FB

Postby fxm » May 21, 2018 12:51

Some member procedures, which have a predefined name in the Type, have a specific role that is constructing, assigning, and destroying objects. These are the constructors, the assignment-operators, and the destructor.
Of these, some are special because a version will be automatically built by the compiler if they are needed and not explicitly defined by the user. These are the default-constructor, the copy-constructor, the copy-assignment operator, and the destructor.
This article takes into account Type inheritance and virtuality.

The default-constructor and the copy-constructor are specific constructors because of their setting of parameters.
The copy-assignment operator is a specific assignment operator because of its setting of parameters.

As any member, these procedures may have any accessibility, public, protected or private (but a private access does not have much interest, and even can totally prevent any direct or derived object construction).

Syntax for their declarations and definitions:
    - Constructors:
      Declaration within Type:
      Declare Constructor ( [ ... [, ... ] ] )
      Definition outside Type:
      Constructor typename ( [ ... [, ... ] ] )
      .....
      End Constructor
    - Assignment-operators:
      Declaration within Type:
      Declare Operator Let ( ... )
      Definition outside Type:
      Operator typename.Let ( ... )
      .....
      End Operator
    - Destructor:
      Declaration within Type:
      Declare Destructor ( )
      Definition outside Type:
      Destructor typename ( )
      .....
      End Destructor
1) Constructors
    A constructor is a kind of member procedure that initializes an instance of its Type. A constructor has the same name as the Type and no return value. A constructor can have any number of parameters and a Type may have any number of overloaded constructors.

    If not any constructor is explicitly defined, the compiler will generate (if needed) a default-constructor that takes no parameters. The user can cancel this behavior by declaring and defining explicitly its own default-constructor.
    If the user defines his own constructor(s), implicit default-initialization operations are always done prior to the user's constructor(s) code execution.

    Order of Construction:
    • The Type constructor is called.
    • The base Types and their member constructors are called in the order of declaration.
    • If the Type has virtual member procedures (including these inherited from its Base), the virtual-pointer (vptr) is set to point to the virtual-table (vtable) of the Type.
    • The body of the Type constructor procedure is executed.
2) Conversion-constructors
    If a Type has a constructor with a single parameter, or if at least all other parameters have a default value, the type of the first argument can be implicitly converted to the type of the Type by the compiler.
    In such expressions, the term 'variable' may be considered as a short-cut of 'typename( variable )'.

    Such conversions can be useful in some cases, but more often they can lead to poor readability (in these cases, it is better to explicitly call the matching constructor).

    Example of implicit conversion:

      Code: Select all

      Type UDT Extends Object
        Declare Constructor ()
        Declare Constructor (Byval i0 As Integer)
        Declare Destructor ()
        Dim As Integer i
      End Type

      Constructor UDT ()
        Print "   => UDT.default-constructor", @This
      End Constructor

      Constructor UDT (Byval i0 As Integer)
        Print "   => UDT.conversion-constructor", @This
        This.i = i0
      End Constructor

      Function RtnI (Byref u As UDT) As Integer
        Return u.i
      End Function
       
      Destructor UDT ()
        Print "   => UDT.destructor", , @This
      End Destructor

      Scope
        Print "Construction: 'Dim As UDT u'"
        Dim As UDT u
        Print
        Print "Assignment: 'u = 123'"
        Print "      " & RtnI(123)            ''  RtnI(123): implicite conversion using the conversion-constructor,
      '                                       ''             short_cut of RtnI(UDT(123))
        Print
        Print "Going out scope: 'End Scope'"
      End Scope

      Sleep

      Code: Select all

      Construction: 'Dim As UDT u'
         => UDT.default-constructor             1703588

      Assignment: 'u = 123'
         => UDT.conversion-constructor          1703580
            123
         => UDT.destructor                      1703580

      Going out scope: 'End Scope'
         => UDT.destructor                      1703588
    But such conversions can even lead to subtle but serious errors in the code.

    Example of subtle/serious errors in the code:

      Code: Select all

      Type point2D
        Declare Constructor (Byval x0 As Integer = 0, Byval y0 As Integer = 0)
        Declare Operator Cast () As String
        Dim As Integer x, y
      End Type

      Constructor point2D (Byval x0 As Integer = 0, Byval y0 As Integer = 0)
        This.x = x0
        This.y = y0
      End constructor

      Operator point2D.cast () As String
        Return "(" & This.x & ", " & This.y & ")"
      End Operator

      Operator + (Byref v0 As point2D, Byref v1 As point2D) As point2D
        Return Type(v0.x + v1.x, v0.y + v1.y)
      End Operator

      Operator * (Byref v0 As point2D, Byref v1 As point2D) As Integer
        Return v0.x * v1.x + v0.y * v1.y
      End Operator

      Print "Construction of v1: 'Dim As point2D v1 = point2D(2, 3)'"
      Dim As point2D v1 = point2D(2, 3)
      Print "  => " & v1
      Print
      Print "Addition to v1: 'v1 + 4'"
      Print "  => " & v1 + 4                  ''  4: implicite conversion using the conversion-constructor,
      '                                       ''             short_cut of point2D(4, ) or point2D(4)
      Print
      Print "Multiplication of v1: 'v1 * 5'"
      Print "  => " & v1 * 5                  ''  5: implicite conversion using the conversion-constructor,
      '                                       ''             short_cut of point2D(5, ) or point2D(5)
      Sleep

      Code: Select all

      Construction of v1: 'Dim As point2D v1 = point2D(2, 3)'
        => (2, 3)

      Addition to v1: 'v1 + 4'
        => (6, 3)

      Multiplication of v1: 'v1 * 5'
        => 10
        In this case, it is dangerous to declare a multi-parameter constructor with optional parameters, because the compiler can interpret one of its forms as a conversion-constructor.
      A workaround: define a default constructor, and the multi-parameter constructor but without optional parameters (or at least one non-optional parameter without considering the first one).
      For other comment about risk due to implicit conversion, see the feature request: #291 Add optional specifier 'Explicit' for constructor declaration to inhibit use by compiler in implicit conversions
    3) Default-constructor
      The default-constructor has no parameters.
      It follows slightly different rules:
      • The default-constructor is one of the special member functions.
      • If no constructors are explicitly declared in a Type, the compiler provides a default-constructor.
      • If any non default-constructors are declared, the compiler does not provide a default-constructor and the user is forced to declare one.
    4) Copy-constructor
      The copy-constructor is a special member procedure that takes as input a reference to an object of the same type, and constructs a new object by copying it.
      If the user does not declare a copy-constructor, the compiler generates (if needed) a copy-constructor for him. The declaration of a copy-assignment operator does not remove the generation by the compiler of a copy-constructor.

      A copy-constructor must be defined if the implicit copy construction is not sufficient. This happens in cases when the object manages dynamically allocated memory or other resources which need to be specially constructed or copied (for example if a member pointer points to dynamically allocated memory, the implicit copy construction will simply do an implicit pointer construction and a value copying instead of allocate memory and then perform the copy of data).

      If the user implements a copy-constructor, it is recommend to also implement a copy-assignment operator so that the meaning of the code is clear.

      In addition to the explicit syntax for calling the copy-constructor, this one is also called when an object is passed by value to a procedure, and when a function returns an object by value by using the 'Return' keyword.
    5) Copy-assignment operator
      The copy-assignment operator is a special member procedure that takes as input a reference to an object of the same type, and makes a copy of it (without creating a new object).
      If the user does not declare a copy-assignment operator, the compiler generates (if needed) a copy-assignment operator for him. The declaration of a copy-constructor does not remove the generation by the compiler of a copy-assignment operator.

      A copy-assignment operator must be defined if the implicit copy is not sufficient. This happens in cases when the object manages dynamically allocated memory or other resources which need to be specially copied (for example if a member pointer points to dynamically allocated memory, the implicit assignment operator will simply copy the pointer value instead of allocate memory and then perform the copy of data).

      If the user implements a copy-assignment operator, it is recommend to also implement a copy-constructor so that the meaning of the code is clear.

      In addition to the explicit syntax for calling the copy-assignment operator, this one is also called when a function returns an object by value by using the 'Function =' keyword.
    6) Destructor
      The destructor function is the inverse of constructor function. It is called when an objects is destroyed.
      The destructor is commonly used to "clean up" when an object is no longer necessary. The destructor cannot have parameters.

      If none destructor is explicitly defined, the compiler will generate (if needed) a destructor. The user can override this behavior by declaring and defining explicitly its own destructor.
      If the user defines his own destructor, implicit destruction operations are always done posterior to the user's destruction code execution.

      The destructor is called when one of the following events occurs:
      • An object created using the New operator is explicitly destroyed using the Delete operator.
      • A local object with block scope goes out of scope.
      • A program terminates and global or static objects exist.
      If a base Type or data member has an accessible destructor, and if the Type does not declare a destructor, the compiler generates one.

      Order of destruction:
      • The Type destructor is called
      • If the Type has virtual member procedures (including these inherited from its Base), the virtual-pointer (vptr) is set to point to the virtual-table (vtable) of the Type.
      • The body of the Type destructor procedure is executed.
      • Destructors for base Types are called in the reverse order of declaration.
    7) Virtual procedures calls in constructors and destructor
      It is usually safe to call any member procedure from within a constructor or destructor because the object is completely set up (virtual tables are initialized and so on) prior to the execution of the first line of user code. However, it is potentially unsafe for a member procedure to call a virtual member procedure for an abstract base Type during construction or destruction.

      Be careful when calling virtual procedures in constructors or destructor, because the procedures that are called in the base constructors or destructor is the base Type versions, not the derived Type versions. When such a virtual procedure is called, the procedure invoked is the procedure defined for the constructor's or destructor's own Type (or inherited from its Bases).
    8) Compiler interaction with default-constructor, copy-constructor, and copy-assignment operator
      During assignments or copy-constructions, the compiler interacts with the different calls of copy-assignment operators, default-constructors and copy-constructors, in order to optimize the copy for resulting objects, making the best use of the member procedures provided by the user (maybe non-exhaustively).

      For an assignment ('object2 = object1'):

      Code: Select all

      '   If (Type has a copy-assignment operator) Then
      '   |  => the copy-assignment opertator of Type is called
      '   Else
      '   |  If (Type has a Base with default-constructor) AND (Type has a Base with copy-assignment operator) Then
      '   |  |  => the copy-assignment operator of Base is called (for Base fields)
      '   |  |  => a shallow-copy is done on only Type fields
      '   |  Else
      '   |  |  => a full shallow-copy is done on all fields
      '   |  End If
      '   End If
      For a copy-construction ('Dim As typename object2 = object1'):

      Code: Select all

      '   If (Type has a Base with default-constructor) Then
      '   |  => the default-constructor of Base is called
      '   End If
      '   If (Type has a copy-constructor) Then
      '   |  => the copy-constructor of Type is called
      '   Else
      '   |  => the Type object is impliciyly default-constructed (even if exists an explicit Type default-constructor)
      '   |  If (Type has a copy-assignment operator) Then
      '   |  |  If (Type has an object field with a default-constructor) OR (Type has a Base with default-constructor) Then
      '   |  |  |  => the copy-assignment operator of Type is called
      '   |  |  Else
      '   |  |  |  => a full shallow-copy is done on all fields
      '   |  |  End If
      '   |  Else
      '   |  |  If (Type has a Base with default-constructor) AND (Type has a Base with copy-assignment operator) Then
      '   |  |  |  => the copy-assignment operator of Base is called (for Base fields)
      '   |  |  |  => a shallow-copy is done on only Type fields
      '   |  |  Else
      '   |  |  |  => a full shallow-copy is done on all fields
      '   |  |  End If
      '   |  End If
      '   End If

      Notes:
        A Type has a default-constructor if one among the following propositions is verified:
        • It has an explicit default-constructor
        • One at least of its field has an initializer.
        • One at least of its members is an object having a default-constructor itself.
        • Its Base (if exists) has a default-constructor (the built-in OBJECT has a default-constructor).
        Under certain conditions, the explicit copy-assignment operator may be called for a copy-construction (if there is not an explicit copy-constructor).
        But inversely, the explicit copy-constructor will never be called for an assignment (if there is not an explicit copy-assignment operator) regardless of the conditions.
      Example to highlight/validate this interaction with default-constructor, copy-constructor, and copy-assignment operator, during assignments or copy-constructions:
        By commenting or not, as you want, independently each of the first 7 code lines ('#define UDT...'), you can test all conditions of the above algorithms:

        Code: Select all

        #define UDTbase_copy_assignment_operator
        #define UDTbase_default_constructor
        #define UDTbase_copy_constructor

        #define UDTderived_copy_assignment_operator
        #define UDTderived_default_constructor
        #define UDTderived_copy_constructor
        #define UDTderived_object_field_with_constructor


        Type UDTbase
          #ifdef UDTbase_copy_assignment_operator
          Declare Operator Let (Byref u1 As UDTbase)
          #endif
          #ifdef UDTbase_copy_constructor
          Declare Constructor ()
          Declare Constructor (Byref u1 As UDTbase)
          #endif
          #ifdef UDTbase_default_constructor
          #ifndef UDTbase_copy_constructor
          Declare Constructor ()
          #endif
          #endif
          Declare Destructor ()
          Dim As Integer i1
        End Type

        #ifdef UDTbase_copy_assignment_operator
        Operator UDTbase.Let (Byref u1 As UDTbase)
          Print "   => UDTbase.copy-assignment", @This & " from " & @u1
          This.i1 = u1.i1
        End Operator
        #endif
        #ifdef UDTbase_copy_constructor
        Constructor UDTbase ()
          Print "   => UDTbase.default-constructor", @This
        End Constructor
        Constructor UDTbase (Byref u1 As UDTbase)
          Print "   => UDTbase.copy-constructor", @This & " from " & @u1
          This.i1 = u1.i1
        End Constructor
        #endif
        #ifdef UDTbase_default_constructor
        #ifndef UDTbase_copy_constructor
        Constructor UDTbase ()
          Print "   => UDTbase.default-constructor", @This
        End Constructor
        #endif
        #endif
        Destructor UDTbase ()
          Print "   => UDTbase.destructor", , @This
        End Destructor

        Type UDTderived Extends UDTbase
          #ifdef UDTderived_copy_assignment_operator
          Declare Operator Let (Byref u2 As UDTderived)
          #endif
          #ifdef UDTderived_copy_constructor
          Declare Constructor ()
          Declare Constructor (Byref u2 As UDTderived)
          #endif
          #ifdef UDTderived_default_constructor
          #ifndef UDTderived_copy_constructor
          Declare Constructor ()
          #endif
          #endif
          Declare Destructor ()
          Dim As Integer i2
          #ifdef UDTderived_object_field_with_constructor
          Dim As String s2
          #endif
        End Type

        #ifdef UDTderived_copy_assignment_operator
        Operator UDTderived.Let (Byref u2 As UDTderived)
          Print "   => UDTderived.copy-assignment", @This & " from " & @u2
          This.i2 = u2.i2
          This.i1 = u2.i1
        End Operator
        #endif
        #ifdef UDTderived_copy_constructor
        Constructor UDTderived ()
          Print "   => UDTderived.default-constructor", @This
        End Constructor
        Constructor UDTderived (Byref u2 As UDTderived)
          Print "   => UDTderived.copy-constructor", @This & " from " & @u2
          This.i2 = u2.i2
          This.i1 = u2.i1
        End Constructor
        #endif
        #ifdef UDTderived_default_constructor
        #ifndef UDTderived_copy_constructor
        Constructor UDTderived ()
          Print "   => UDTderived.default-constructor", @This
        End Constructor
        #endif
        #endif
        Destructor UDTderived ()
          Print "   => UDTderived.destructor", , @This
        End Destructor

        Scope
          Print "Construction: 'Dim As UDTderived a, b : a.i1 = 1 : a.i2 = 2'"
          Dim As UDTderived a, b : a.i1 = 1 : a.i2 = 2
          Print "      " & a.i1
          Print "      " & a.i2
          Print
          Print "Assignment: 'b = a'"
          b = a
          Print "      " & b.i1
          Print "      " & b.i2
          Print
          Print "Copy-construction: 'Dim As UDTderived c = a'"
          Dim As UDTderived c = a
          Print "      " & c.i1
          Print "      " & c.i2
          Print
          Print "Going out scope: 'End Scope'"
        End Scope

        Sleep

        Code: Select all

        Construction: 'Dim As UDTderived a, b : a.i1 = 1 : a.i2 = 2'
           => UDTbase.default-constructor         1703576
           => UDTderived.default-constructor      1703576
           => UDTbase.default-constructor         1703556
           => UDTderived.default-constructor      1703556
              1
              2

        Assignment: 'b = a'
           => UDTderived.copy-assignment          1703556 from 1703576
              1
              2

        Copy-construction: 'Dim As UDTderived c = a'
           => UDTbase.default-constructor         1703488
           => UDTderived.copy-constructor         1703488 from 1703576
              1
              2

        Going out scope: 'End Scope'
           => UDTderived.destructor               1703488
           => UDTbase.destructor                  1703488
           => UDTderived.destructor               1703556
           => UDTbase.destructor                  1703556
           => UDTderived.destructor               1703576
           => UDTbase.destructor                  1703576

See also:
- How to right Use Dynamic (variable-length) Arrays in FB UDTs

Return to “Documentation”

Who is online

Users browsing this forum: No registered users and 1 guest