【问题标题】:With a class operator is an implicit typecast to itself allowed?使用类运算符是否允许对其自身进行隐式类型转换?
【发布时间】:2013-09-14 22:30:44
【问题描述】:

我的记录如下:

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-assembly 中。

我想通过下面的代码来触发底部的类操作符:

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;

(如果它按我的预期工作,将测试并添加答案)。

【问题讨论】:

  • 我看不出你怎么能有一个从类型 A 到自身的隐式转换。并不是说我能猜出你的意图。无法想象你为什么要打包你的唱片。你想让代码变慢吗?
  • @David,他想要一个复制构造函数,因为他的数据结构没有引用计数。
  • @Rob 好的。不过不会发生。动态数组也无济于事。我认为获得分配以执行副本的唯一方法是使您的类型成为一个值。字符串 COW 的特殊豁免。
  • @DavidHeffernan 该代码是遗留代码,我无法更改它而不破坏所有内容。
  • 删除了 packed 记录引用,因为它与问题无关。也会将其从代码中删除,事实证明这并不难。

标签: delphi operator-overloading


【解决方案1】:

我的first answer 试图劝阻不要重写赋值运算符的想法。我仍然支持这个答案,因为要遇到的许多问题都可以通过对象更好地解决。

但是,David 非常正确地指出,TBigInt 是作为记录实现的,以利用运算符重载。 IE。 a := b + c;。这是坚持基于记录的实现的一个很好的理由。

因此,我提出了一种用一块石头杀死两只鸟的替代解决方案:

  • 它消除了我在其他答案中解释的内存管理风险。
  • 并提供了一种简单的机制来实现 Copy-on-Write 语义。

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

大致思路如下:

  • 定义一个接口来表示 BigInt 数据。 (这最初可以是极简的,并且仅支持对指针的控制 - 就像在我的示例中一样。这将使现有代码的初始转换更容易。)
  • 定义将由TBigInt 记录使用的上述接口的实现。
  • 接口解决了第一个问题,因为接口是托管类型;当记录超出范围时,Delphi 将取消对接口的引用。因此,底层对象将在不再需要时自行销毁。
  • 该界面还提供了解决第二个问题的机会,因为我们可以通过查看RefCount 来了解是否应该Copy-On-Write。
  • 请注意,从长远来看,将部分 BigInt 实现从记录移至类和接口可能会被证明是有益的。

以下代码是精简的“big int”实现,纯粹是为了说明这些概念。 (即“大”整数仅限于常规的 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;

以下测试是在我构建建议的解决方案时编写的。他们证明:一些基本功能,写时复制按预期工作,并且没有内存泄漏。

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。我意识到还有另一种选择,可能更好,那就是使类型不可变。这完全消除了这个问题。
【解决方案2】:

Delphi 中没有任何东西可以让您挂钩到分配过程。 Delphi 没有 C++ 复制构造函数。

您的要求是:

  1. 您需要对数据的引用,因为它是可变长度的。
  2. 您还需要值语义。

满足这两个要求的唯一类型是本机 Delphi 字符串类型。它们被实现为参考。但是他们的写时复制行为赋予了他们价值语义。既然你想要一个字节数组,那么 AnsiString 就是满足你需要的字符串类型。

另一种选择是简单地使您的类型不可变。这将使您不必担心复制引用,因为引用的数据永远不会被修改。

【讨论】:

  • RawByteString 不是更合适(适用于支持 UniCode 的 Delphi 编译器)吗?
  • @Heartware 根据文档 RawByteString 仅用于参数。我认为 AnsiString 很好,因为您永远不会执行导致转换的操作。唯一的操作是赋值和元素访问。
  • 这个答案也许应该警告如果字符串或其任何元素被类型转换以进行修改,则写入时复制不会自动启动。例如。 Byte(FStringData[I]) := N 此类代码很可能在 TBigInt 实现中,在这种情况下,应事先通过 UniqueString 强制执行手动 COW。注意:如果条目被修改而没有对字符串进行类型转换,则将应用自动 COW:例如FStringData[I] := AnsiChar(N)。每次修改都会检查 RefCount;如有必要,最多第一个将创建一个唯一的副本。
  • PS:同意,不可变会更安全。
  • @Craig 是的。我曾想过,但在我写下答案之后。我正在考虑使用我的 N×N 矩阵类的端口来实现这种方法。
【解决方案3】:

在我看来,您的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.

【讨论】:

  • 您想要一个记录,以便您可以进行复制分配和编写运算符。您希望能够编写 a := b + c 并正确表达自己。
  • @DavidHeffernan 好的,同意。然后它似乎有点像一个catch-22。我仍然坚持这个答案,因为在内存管理方面所要求的内容将极具风险......(但是,我确实有另一个想法......)
  • 根据我的回答,除了写时复制之外,我没有看到任何可行的选择。如果您想要值语义和可变大小的有效负载,则不需要。
  • @DavidHeffernan 您对 Copy-On-Write 的看法是正确的,只是您不限于字符串...提到的另一个想法是使用接口的 roll-your-own COW
  • 好吧,滚动你自己的牛是一种选择,但使用内置的牛肯定更容易。忽略这个久经考验的机制似乎有点奇怪。归根结底,将AnsiString 视为一个花哨的字节数组也没什么大不了的。
猜你喜欢
  • 2012-10-02
  • 2014-09-08
  • 1970-01-01
  • 2011-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-25
相关资源
最近更新 更多