使用类操作符,是否允许对自身进行隐式类型转换?

3

我有一条记录,看起来像这样:

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

这段代码是预类操作符遗留代码,但我想要添加操作符。

我知道数据应该存储在一个动态字节数组中,但我不想改变代码,因为所有的核心都在x86汇编中。

我希望以下代码可以触发底部的类操作符:

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

如果我给自己添加隐式类型转换,以下代码是否会被执行?

class operator TBigint.Implicit(a: TBigint): TBigint;
begin
  sdpBigint.CreateBigint(Result, a.Size);
  sdpBigint.CopyBigint(a, Result);
end;
< p > < em >(如果符合我的预期,我将测试并添加答案)。< /em > < /p > (如果结果符合预期,我会测试并添加答案。)

我不明白如何将类型A隐式转换为它本身。我无法猜测您的意图。我不知道为什么要压缩记录。您想让代码变慢吗? - David Heffernan
1
@David,他想要一个复制构造函数,因为他的数据结构没有引用计数。 - Rob Kennedy
@Rob 好的,不过不可能发生。动态数组也没有帮助。我认为让赋值执行拷贝的唯一方法是将您的类型设置为值类型。字符串 COW 有特殊豁免。 - David Heffernan
@DavidHeffernan 这段代码是遗留代码,我不能随意更改它,否则会导致各种问题。 - Johan
从代码中删除了“packed”记录的引用,因为它与问题无关。也会从代码中将其删除,结果证明这并不难。 - Johan
3个回答

4
我的第一个答案试图阻止覆盖赋值运算符的想法。我仍然坚持那个答案,因为许多问题最好通过对象来解决。
但是,David非常正确地指出,TBigInt被实现为记录以利用运算符重载。即a := b + c;。这是坚持基于记录的实现的一个非常好的理由。
因此,我提出了这个杀两只鸟的替代方案:
  • 它消除了我其他答案中解释的内存管理风险。
  • 并提供了一种实现写时复制语义的简单机制。

(我仍然建议除非有很好的理由保留基于记录的解决方案,否则考虑切换到基于对象的解决方案。)

总体思路如下:

  • 定义一个接口来表示BigInt数据。(最初可以是最小化的,仅支持指针控制 - 如我的示例。这将使现有代码的初始转换更容易。)
  • 定义上述接口的实现,将由TBigInt记录使用。
  • 接口解决了第一个问题,因为接口是托管类型;当记录超出作用域时,Delphi将取消引用接口。因此,不再需要时底层对象将销毁自身。
  • 接口还提供了解决第二个问题的机会,因为我们可以检查RefCount来知道是否应该进行写时复制。
  • 请注意,从长远来看,将BigInt实现的一些部分从记录移动到类和接口可能会证明有益。

以下代码是缩减版的"big int"实现,仅用于说明概念。(即"big"整数限制为常规32位数字,并且只实现了加法。)

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;

以下测试是在我构建提议的解决方案时编写的。它们证明了一些基本功能,拷贝-写入(copy-on-write)按预期工作,并且没有内存泄漏。
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;

编辑

添加了一个测试,演示如何将TBigInt作为值参数传递给一个过程。

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;

做得相当不错。我仍然认为我会采用简单的选项并使用内置的COW。我意识到还有另一种选项,可能更好,那就是使类型不可变。这样就完全解决了问题。 - David Heffernan

2

在Delphi中没有任何东西可以让你钩入赋值过程。Delphi没有像C++的复制构造函数。

您的要求是:

  1. 由于数据长度可变,您需要对数据进行引用。
  2. 您还需要值语义。

唯一满足这两个要求的类型是原生的Delphi字符串类型。它们实现为引用。但是,它们具有的写入时复制行为使它们具有值语义。由于您想要一个字节数组,因此AnsiString是满足您需求的字符串类型。

另一个选择是简单地让您的类型不可变。这样可以让您不再担心复制引用,因为所引用的数据永远无法被修改。


对于启用了Unicode的Delphi编译器,使用RawByteString更合适吧? - HeartWare
根据文档,RawByteString仅用于参数。我认为AnsiString也可以,因为您永远不会执行导致转换的操作。唯一的操作是赋值和元素访问。 - David Heffernan
这个答案或许应该警告说,如果字符串或其任何元素被强制转换进行修改,则不会自动启用写时复制。例如 Byte(FStringData[I]) := N 这样的代码在 TBigInt 实现中很可能存在,在这种情况下,应该通过 UniqueString 手动强制执行 COW。注意:如果在不强制类型转换字符串的情况下修改条目,则将应用自动 COW:例如 FStringData[I] := AnsiChar(N)。每次修改都会检查 RefCount;如果必要,最多只会创建一个唯一副本。 - Disillusioned
同意,不可变会更安全。 - Disillusioned
@Craig 是的。我曾经想过这个,但是在回答之后。我在考虑将我的N乘N矩阵类移植到这个方法中进行尝试。 - David Heffernan

1
我认为你的TBigInt应该是一个类而不是记录。因为你关心PtrDigits被缠绕在一起,所以看起来你需要额外的内存管理来处理指针引用的内容。由于记录不支持析构函数,因此没有自动管理该内存。另外,如果你只声明了一个TBigInt变量,但没有调用CreatBigInt构造函数,则内存不会正确初始化。同样,这是因为你不能覆盖记录的默认无参数构造函数。
基本上,你必须始终记住为记录分配了什么,并记得手动释放。当然,你可以在记录上有一个释放过程来帮助处理这个问题,但你仍然必须记得在正确的位置调用它。
然而,你可以实现一个显式的Copy函数,并将TBitInt的复制正确性添加到你的代码审查清单中。不幸的是,你必须非常小心隐含的拷贝,比如通过值参数将记录传递给另一个例程。
下面的代码演示了一个与你需求类似的概念示例,并展示了CreateCopy函数如何“解开”指针。它还凸显出一些内存管理问题,这就是为什么记录可能不是一个好办法的原因。
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;

简而言之,看起来你试图让记录进行它们并不真正适合的任务。
它们非常擅长制作精确的副本。它们具有简单的内存管理:声明一个记录变量,所有内存都被分配。变量超出范围,所有内存都被释放。

编辑

演示重载赋值运算符可能导致内存泄漏的示例。

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.

2
你需要一个记录,以便可以进行复制赋值和写操作符。你希望能够写成 a := b + c 的形式,以便正确地表达自己。 - David Heffernan
@DavidHeffernan 好的,我同意。那么这似乎是一个进退两难的局面。基于所要求的内容在内存管理方面极其危险,我仍然坚持我的答案......(不过,我有另一个想法...) - Disillusioned
根据我的回答,我没有看到除了写时复制之外的其他可行选项。如果您想要值语义和可变大小的有效载荷,那就只能这样了。 - David Heffernan
@DavidHeffernan 您关于Copy-On-Write的看法是正确的,只是您不仅限于字符串...另一个提到的想法是使用接口roll-your-own COW - Disillusioned
打造你自己的COW是一种选择,但使用内置的COW肯定更容易。忽略那个经过非常良好的测试和验证机制似乎有点奇怪。最终,将AnsiString视为不过是一种花哨的字节数组并对其进行处理并不是太困难。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接