Johan
Johan

Reputation: 76597

With a class operator is an implicit typecast to itself allowed?

I have a record that looks like:

TBigint = record
    PtrDigits: Pointer;                  <-- The data is somewhere else.
    Size: Byte;
    MSB: Byte;
    Sign: Shortint;
    ...
    class operator Implicit(a: TBigint): TBigint;  <<-- is this allowed?
    ....

The code is pre-class operator legacy code, but I want to add operators.

I know the data should really be stored in a dynamic array of byte, but I do not want to change the code, because all the meat is in x86-assembly.

I want to following code to trigger the class operator at the bottom:

procedure test(a: TBignum);
var b: TBignum;
begin
  b:= a;  <<-- naive copy will tangle up the `PtrDigit` pointers.
  ....

If I add the implicit typecast to itself, will the following code be executed?

class operator TBigint.Implicit(a: TBigint): TBigint;
begin
  sdpBigint.CreateBigint(Result, a.Size);
  sdpBigint.CopyBigint(a, Result);
end;

(Will test and add the answer if it works as I expect).

Upvotes: 3

Views: 2537

Answers (3)

David Heffernan
David Heffernan

Reputation: 613003

There is nothing in Delphi that allows you to hook into the assignment process. Delphi has nothing like C++ copy constructors.

Your requirements, are that:

  1. You need a reference to the data, since it is of variable length.
  2. You also have a need for value semantics.

The only types that meet both of those requirements are the native Delphi string types. They are implemented as a reference. But the copy-on-write behaviour that they have gives them value semantics. Since you want an array of bytes, AnsiString is the string type that meets your needs.

Another option would be to simply make your type be immutable. That would let you stop worrying about copying references since the referenced data could never be modified.

Upvotes: 2

Disillusioned
Disillusioned

Reputation: 14832

My first answer attempts to dissuade against the idea of overriding the assignment operator. I still stand by that answer, because many of the problems to be encountered are better solved with objects.

However, David quite rightly pointed out that TBigInt is implemented as a record to leverage operator overloads. I.e. a := b + c;. This is a very good reason to stick with a record based implementation.

Hence, I propose this alternative solution that kills two birds with one stone:

  • It removes the memory management risks explained in my other answer.
  • And provides a simple mechanism to implement Copy-on-Write semantics.

(I do still recommend that unless there's a very good reason to retain a record based solution, consider switching to an object based solution.)

The general idea is as follows:

  • Define an interface to represent the BigInt data. (This can initially be minimalist and support only control of the pointer - as in my example. This would make the initial conversion of existing code easier.)
  • Define an implementation of the above interface which will be used by the TBigInt record.
  • The interface solves the first problem, because interfaces are a managed type; and Delphi will dereference the interface when a record goes out of scope. Hence, the underlying object will destroy itself when no longer needed.
  • The interface also provides the opportunity to solve the second problem, because we can check the RefCount to know whether we should Copy-On-Write.
  • Note that long term it might prove beneficial to move some of the BigInt implementation from the record to the class & interface.

The following code is trimmed-down "big int" implementation purely to illustrate the concepts. (I.e. The "big" integer is limited to a regular 32-bit number, and only addition has been implemented.)

type
  IBigInt = interface
    ['{1628BA6F-FA21-41B5-81C7-71C336B80A6B}']
    function GetData: Pointer;
    function GetSize: Integer;
    procedure Realloc(ASize: Integer);
    function RefCount: Integer;
  end;

type
  TBigIntImpl = class(TInterfacedObject, IBigInt)
  private
    FData: Pointer;
    FSize: Integer;
  protected
    {IBigInt}
    function GetData: Pointer;
    function GetSize: Integer;
    procedure Realloc(ASize: Integer);
    function RefCount: Integer;
  public
    constructor CreateCopy(ASource: IBigInt);
    destructor Destroy; override;
  end;

type
  TBigInt = record
    PtrDigits: IBigInt;
    constructor CreateFromInt(AValue: Integer);
    class operator Implicit(AValue: TBigInt): Integer;
    class operator Add(AValue1, AValue2: TBigInt): TBigInt;
    procedure Add(AValue: Integer);
  strict private
    procedure CopyOnWriteSharedData;
  end;

{ TBigIntImpl }

constructor TBigIntImpl.CreateCopy(ASource: IBigInt);
begin
  Realloc(ASource.GetSize);
  Move(ASource.GetData^, FData^, FSize);
end;

destructor TBigIntImpl.Destroy;
begin
  FreeMem(FData);
  inherited;
end;

function TBigIntImpl.GetData: Pointer;
begin
  Result := FData;
end;

function TBigIntImpl.GetSize: Integer;
begin
  Result := FSize;
end;

procedure TBigIntImpl.Realloc(ASize: Integer);
begin
  ReallocMem(FData, ASize);
  FSize := ASize;
end;

function TBigIntImpl.RefCount: Integer;
begin
  Result := FRefCount;
end;

{ TBigInt }

class operator TBigInt.Add(AValue1, AValue2: TBigInt): TBigInt;
var
  LSum: Integer;
begin
  LSum := Integer(AValue1) + Integer(AValue2);
  Result.CreateFromInt(LSum);
end;

procedure TBigInt.Add(AValue: Integer);
begin
  CopyOnWriteSharedData;

  PInteger(PtrDigits.GetData)^ := PInteger(PtrDigits.GetData)^ + AValue;
end;

procedure TBigInt.CopyOnWriteSharedData;
begin
  if PtrDigits.RefCount > 1 then
  begin
    PtrDigits := TBigIntImpl.CreateCopy(PtrDigits);
  end;
end;

constructor TBigInt.CreateFromInt(AValue: Integer);
begin
  PtrDigits := TBigIntImpl.Create;
  PtrDigits.Realloc(SizeOf(Integer));
  PInteger(PtrDigits.GetData)^ := AValue;
end;

class operator TBigInt.Implicit(AValue: TBigInt): Integer;
begin
  Result := PInteger(AValue.PtrDigits.GetData)^;
end;

The following tests were written as I built up the proposed solution. They prove: some basic functionality, that the copy-on-write works as expected, and that there are no memory leaks.

procedure TTestCopyOnWrite.TestCreateFromInt;
var
  LBigInt: TBigInt;
begin
  LBigInt.CreateFromInt(123);
  CheckEquals(123, LBigInt);
  //Dispose(PInteger(LBigInt.PtrDigits)); //I only needed this until I 
                                          //started using the interface
end;

procedure TTestCopyOnWrite.TestAssignment;
var
  LValue1: TBigInt;
  LValue2: TBigInt;
begin
  LValue1.CreateFromInt(123);
  LValue2 := LValue1;
  CheckEquals(123, LValue2);
end;

procedure TTestCopyOnWrite.TestAddMethod;
var
  LValue1: TBigInt;
begin
  LValue1.CreateFromInt(123);
  LValue1.Add(111);

  CheckEquals(234, LValue1);
end;

procedure TTestCopyOnWrite.TestOperatorAdd;
var
  LValue1: TBigInt;
  LValue2: TBigInt;
  LActualResult: TBigInt;
begin
  LValue1.CreateFromInt(123);
  LValue2.CreateFromInt(111);

  LActualResult := LValue1 + LValue2;

  CheckEquals(234, LActualResult);
end;

procedure TTestCopyOnWrite.TestCopyOnWrite;
var
  LValue1: TBigInt;
  LValue2: TBigInt;
begin
  LValue1.CreateFromInt(123);
  LValue2 := LValue1;

  LValue1.Add(111); { If CopyOnWrite, then LValue2 should not change }

  CheckEquals(234, LValue1);
  CheckEquals(123, LValue2);
end;

Edit

Added a test demonstrating use of TBigInt as value parameter to a procedure.

procedure TTestCopyOnWrite.TestValueParameter;
  procedure CheckValueParameter(ABigInt: TBigInt);
  begin
    CheckEquals(2, ABigInt.PtrDigits.RefCount);
    CheckEquals(123, ABigInt);
    ABigInt.Add(111);
    CheckEquals(234, ABigInt);
    CheckEquals(1, ABigInt.PtrDigits.RefCount);
  end;
var
  LValue: TBigInt;
begin
  LValue.CreateFromInt(123);
  CheckValueParameter(LValue);
end;

Upvotes: 4

Disillusioned
Disillusioned

Reputation: 14832

It seems to me your TBigInt should be a class rather than a record. Because you're concerned about PtrDigits being tangled up, it sounds like you're needing extra memory management for what the pointer references. Since records don't support destructors there's no automatic management of that memory. Also if you simply declare a variable of TBigInt, but don't call the CreatBigInt constructor, the memory is not correctly initialised. Again, this is because you cannot override a record's default parameterless constructor.

Basically you have to always remember what has been allocated for the record and remember to manually deallocate. Sure you can have a deallocate procedure on the record to help in this regard, but you still have to remember to call it in the correct places.

However that said, you could implement an explicit Copy function, and add an item to your code-review checklist that TBitInt has been copied correctly. Unfortunately you'll have to be very careful with the implied copies such as passing the record via a value parameter to another routine.

The following code illustrates an example conceptually similar to your needs and demonstrates how the CreateCopy function "untangles" the pointer. It also highlights some of the memory management problems that crop up, which is why records are probably not a good way to go.

type
  TMyRec = record
    A: PInteger;
    function CreateCopy: TMyRec;
  end;

function TMyRec.CreateCopy: TMyRec;
begin
  New(Result.A);
  Result.A^ := A^;
end;

var
  R1, R2: TMyRec;
begin
  New(R1.A); { I have to manually allocate memory for the pointer 
               before I can use the reocrd properly.
               Even if I implement a record constructor to assist, I
               still have to remember to call it. }
  R1.A^ := 1;
  R2 := R1;
  R2.A^ := 2; //also changes R1.A^ because pointer is the same (or "tangled")
  Writeln(R1.A^);

  R2 := R1.CreateCopy;
  R2.A^ := 3; //Now R1.A is different pointer so R1.A^ is unchanged
  Writeln(R1.A^);
  Dispose(R1.A);
  Dispose(R2.A); { <-- Note that I have to remember to Dispose the additional 
                   pointer that was allocated in CreateCopy }
end;

In a nutshell, it seems you're trying to sledgehammer records into doing things they're not really suited to doing.
They are great at making exact copies. They have simple memory management: Declare a record variable, and all memory is allocated. Variable goes out of scope and all memory is deallocated.


Edit

An example of how overriding the assignment operator can cause a memory leak.

var
  LBigInt: TBigInt;
begin
  LBigInt.SetValue(123);
  WriteBigInt(LBigInt); { Passing the value by reference or by value depends
                          on how WriteBigInt is declared. }
end;

procedure WriteBigInt(ABigInt: TBigInt);
//ABigInt is a value parameter.
//This means it will be copied.
//It must use the overridden assignment operator, 
//  otherwise the point of the override is defeated.
begin
  Writeln('The value is: ', ABigInt.ToString);
end;
//If the assignment overload allocated memory, this is the only place where an
//appropriate reference exists to deallocate.
//However, the very last thing you want to do is have method like this calling 
//a cleanup routine to deallocate the memory....
//Not only would this litter your code with extra calls to accommodate a 
//problematic design, would also create a risk that a simple change to taking 
//ABigInt as a const parameter could suddenly lead to Access Violations.

Upvotes: 1

Related Questions