Проблемы перегрузки операторов в Delphi

Вольный перевод поста On the operator overloading in Delphiopen in new window.

Перегрузка операторов в Delphi является простой если запись не содержит в себе полей-ссылок на объекты в куче. Чтобы проиллюстрировать эту проблему рассмотрим следующий (некорректный!) пример:

program DelphiDemo;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  Adder = record
  private
    FRef: PInteger;
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  public
    procedure Init(AValue: Integer = 0);
    procedure Done;
    class operator Add(const A, B: Adder): Adder;
    property Memory: Integer read GetMemory write SetMemory;
  end;

{ Adder }

class operator Adder.Add(const A, B: Adder): Adder;
begin
// !!! Утечка памяти
  New(Result.FRef);
  Result.Memory:= A.Memory + B.Memory;
end;

procedure Adder.Done;
begin
  Dispose(FRef);
end;

function Adder.GetMemory: Integer;
begin
  Result:= FRef^;
end;

procedure Adder.Init(AValue: Integer);
begin
  New(FRef);
  FRef^:= AValue;
end;

procedure Adder.SetMemory(AValue: Integer);
begin
  FRef^:= AValue;
end;

procedure Test;
var
  A, B, C: Adder;

begin
  A.Init(1);
  B.Init(2);
  C.Init();
  C := A + B;
  Writeln(C.Memory);
  C.Done;
  B.Done;
  A.Done;
end;

begin
  ReportMemoryLeaksOnShutdown:= True;
  try
    Test;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

Строка #59 (C:= A + B) работает следующим образом:

  • Временная запись Result помещается в стек
  • Временная запись заполняется суммой A + B (метод Adder.Add)
  • Временная запись присваивается (поверхностным копированием) переменной C
  • Временная запись убирается со стека

Код работает правильно если Adder не содержит ссылок на кучу, FRef в экземпляре Adder делает ситуацию более сложной. Вы должны всегда инициализировать поле FRef для каждого экземпляра Adder, но вы не можете финализировать временную запись которая создана на строке #59. (также не можете финализировать запись которая инициализируется в строке #58 и теряется в строке #59).

Единственный путь исправить утечку памяти — закомпостировать строку #58, но это не будет работать в более сложных случаях, например, когда переменная должна участвовать в выражении справа.

Правильное решение использует автоматическое управление памятью вместо простых указателей. Ниже решение использующее интерфейс:

program DelphiDemo2;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes;

type
  IAdder = interface
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  end;

  TAdderRef = class(TInterfacedObject, IAdder)
  private
    FMemory: Integer;
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  end;

  Adder = record
  private
    FRef: IAdder;
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  public
    procedure Init(AValue: Integer = 0);
    procedure Done;
    class operator Add(const A, B: Adder): Adder;
    property Memory: Integer read GetMemory write SetMemory;
  end;

{ TAdderRef }

function TAdderRef.GetMemory: Integer;
begin
  Result:= FMemory;
end;

procedure TAdderRef.SetMemory(AValue: Integer);
begin
  FMemory:= AValue;
end;

{ Adder }

class operator Adder.Add(const A, B: Adder): Adder;
begin
  Result.FRef:= TAdderRef.Create;
  Result.Memory:= A.Memory + B.Memory;
end;

procedure Adder.Init(AValue: Integer);
begin
  FRef:= TAdderRef.Create;
  FRef.SetMemory(AValue);
end;

procedure Adder.Done;
begin
  FRef:= nil;
end;

function Adder.GetMemory: Integer;
begin
  Result:= FRef.GetMemory;
end;

procedure Adder.SetMemory(AValue: Integer);
begin
  FRef.SetMemory(AValue);
end;

procedure Test;
var
  A, B, C: Adder;

begin
  A.Init(1);
  B.Init(2);
//  C.Init();
  C:= A + B;
  Writeln(C.Memory);
//  C.Done;
//  B.Done;
//  A.Done;
end;

begin
  ReportMemoryLeaksOnShutdown:= True;
  try
    Test;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

Полезный побочный эффект при таком подходе в том что вам не нужно инициализировать или финализировать поле FRef вручную (но вы можете это делать). Несколько строк в процедуре Test, показанной выше, закомментированы потому, что они больше не нужны, но они могут быть раскомментированы и код продолжит верно работать — автоматическое управление памятью для интерфейсов обеспечит это.

Очень интересно знать как обсуждаемая проблема решается в C++. Стандартный C++ подход полностью отличается — он требует перегрузки оператора присваивания (возможность, которую Delphi не поддерживает) и написания конструктора копирования (другая концепция отсутствующая в Delphi). Я планирую обсудить это позже.

Последниее изменение: 24.08.2023, 06:42:55