[fpc-pascal] If an Assert fails in constructor, then destructor called (?)

Tom Verhoeff T.Verhoeff at tue.nl
Tue Jun 30 00:19:15 CEST 2009


While tracing a nasty bug (?), I discovered the hard way that when
an Assert is done in a constructor, and it fails, then the destructor
(Destroy) is automatically called.

However, because the constructor failed, the object did not satisfy the
class invariants.

So, the destructor needs to be pessimistic about the state.
Mine was not.

Since the Assert in the constructor is there to protect against
parameter values that do not statisfy the constructor's precondition, 
there are no additional dynamic veriables and objects created, and
hence they need not be destroyed.

How can the destructor know that it is called in such a state?

Below is a simple example to illustrate the problem.
My destructor assumed that the class invariants hold, and it uses some
of the class methods to aid in tearing down the object.  Except
that the object does not exist if the destructor gets called because
of the failed Assert in the constructor.

A painful lesson in Design By Contract...

Is there any place in the documentation, where one can find about
this implicit call of the destructor?  Should it be added?

The program below writes

	FAIL: Raised unexpected exception Access violation

which is raised in the destructor, after the EAssertionFailed excpetion
is raised in the constructor.

When uncommenting the if statement in the destructor, it writes:

	OK: Raised EAssertionFailedTRing.Create pre failed: n = -1 (assert_in_constructor.pas, line 44)

In that case, one needs to rely on the fact that fields in a class get
initialized in a predefined way, e.g. pointers are set to nil.

Best regards,

	Tom Verhoeff
--
program Assert_in_constructor;

{$Mode Delphi}
{$Assertions on}

uses
  SysUtils, Classes;

type

  PCell = ^ TCell;
  TCell = record // singly linked list of integers
    FData: Integer;
    FNext: PCell;
  end;

  TRing = class ( TObject )
  private
    FList: PCell; // the elements in the ring
    // Invariants:
    //   NonEmpty: FList <> nil;
    //   IsRing: repeated application of ^.FNext to FList leads to FList,
    //           i.e., the last cell points to the head cell
    //   Unique: all p^.FData for p: PCell appearing in FList are different
  public
    constructor Create ( n: Integer );
      { pre: 0 < n; post: ring consists of 0 through n-1 }
    
    function IsSingleton: Boolean;
    function Current: Integer;
    procedure Step;
    procedure RemoveSuccessor;
      { pre: not IsSingleton }

    destructor Destroy; override;
  end;

constructor TRing.Create ( n: Integer );
var
  i: Integer;
  q: PCell; // points to last cell, with FData = i-1
begin
  inherited Create;
  Assert ( 0 < n,
           Format ( 'TRing.Create pre failed: n = %D', [ n ] ) );
  New(FList);
  q := FList;
  q^.FData := 0;
  i := 1;

  while i <> n do begin
    New ( q^.FNext );
    q := q^.FNext;
    q^.FData := i;
    Inc ( i );
  end;

  q^.FNext := FList; // ring closed
end;

function TRing.IsSingleton: Boolean;
begin
  Result := FList^.FNext = FList;
end;

function TRing.Current: Integer;
begin
  Result := FList^.FData;
end;

procedure TRing.Step;
begin
  FList := FList^.FNext;
end;
  
procedure TRing.RemoveSuccessor;
var
  q: PCell; // cell to be removed
begin
  q := FList^.FNext;
  FList^.FNext := q^.FNext; // q unlinked
  Dispose ( q );
end;

destructor TRing.Destroy;
begin
  //if FList <> nil then
  while not IsSingleton do RemoveSuccessor;
  Dispose ( FList );
  inherited Destroy;
end;

var
  v: TRing;

begin

  try
    v := TRing.Create ( -1 );
    writeln ( 'FAIL: Should have raised EAssertionFailed' );
  except
    on e : EAssertionFailed do begin
      writeln ( 'OK: Raised EAssertionFailed' + e.Message )
    end;
    on e : Exception do begin
      writeln ( 'FAIL: Raised unexpected exception ' + e.Message )
    end
  else
    writeln ( 'FAIL: Raised an unrecognized exception' )
  end; { try }

end.



More information about the fpc-pascal mailing list