관리 타입과 참조 카운트

관리 타입과 참조 카운트 (Managed types and reference counts)

Pascal을 쓰다 보면 문자열이나 인터페이스 같은 값이 여러 곳에서 공유될 때가 있어요. Free Pascal은 일부 타입을 **관리 타입(managed type)**으로 취급해서, 데이터가 참조되는 횟수(참조 카운트)를 자동으로 관리해요. 이 참조 카운트가 함수/프로시저의 매개변수 한정자에 따라 어떻게 달라지는지 이해하는 게 중요해요.

출처: Free Pascal Reference - Managed types and reference counts

본문

일부 타입 — Unicodestring, Ansistring, 인터페이스, 동적 배열 — 은 컴파일러가 다소 특별하게 취급해요. 이 타입들의 데이터에는 참조 카운트가 있어서, 데이터에 대한 참조가 몇 개 존재하는지에 따라 카운트가 증가하거나 감소해요.

함수나 프로시저 호출에서 매개변수의 한정자가 관리 타입의 참조 카운트에 어떤 영향을 주는지 정리하면 다음과 같아요.

  • 아무것도 (값 전달, pass by value): 매개변수의 참조 카운트는 진입 시 1 증가하고, 종료 시 1 감소해요.
  • out: 전달된 값의 참조 카운트는 1 감소하고, 프로시저에 전달된 변수는 "빈(empty)" 상태로 초기화돼요(보통 Nil이지만, 이는 의존하면 안 되는 구현 세부 사항이에요).
  • var: 참조 카운트에는 아무 일도 일어나지 않아요. 원래 변수에 대한 참조가 전달되고, 그것을 바꾸거나 읽는 것은 원래 변수를 바꾸거나 읽는 것과 정확히 같은 효과를 가져요.
  • const: 이 경우는 조금 까다로워요. 값이 아닌 것들을 전달할 수 있기 때문에 참조 카운트에는 아무 일도 일어나지 않아요. 특히 인터페이스 자체 대신 그 인터페이스를 구현하는 클래스를 전달할 수 있는데, 이러면 클래스가 예상치 못하게 해제될 수 있어요.

참고: 함수 결과는 내부적으로 함수의 var 매개변수처럼 취급되고, var 매개변수와 같은 규칙이 적용돼요.

다음 예시는 그 위험성을 보여줘요.

{$mode objfpc}

Type
  ITest = Interface
    Procedure DoTest(ACount : Integer);
  end;

  TTest = Class(TInterfacedObject,ITest)
    Procedure DoTest(ACount : Integer);
    Destructor destroy; override;
  end;

Destructor TTest.Destroy;

begin
  Writeln('Destroy called');
end;

Procedure TTest.DoTest(ACount : Integer);

begin
  Writeln('Test ',ACount,' : ref count: ',RefCount);
end;

procedure DoIt1(x: ITest; ACount : Integer);

begin
  // Reference count is increased
  x.DoTest(ACount);
  // And decreased
end;

procedure DoIt2(const x: ITest; ACount : Integer);

begin
  // No change to reference count.
  x.DoTest(ACount);
end;

Procedure Test1;

var
  y: ITest;
begin
  y := TTest.Create;
  // Ref. count is 1 at this point.
  y.DoTest(1);
  // Calling DoIT will increase reference count and decrease on exit.
  DoIt1(y,2);

  // Reference count is still one.
  y.DoTest(3);
end;

Procedure Test2;

var
  Y : TTest;
begin
  Y := TTest.Create; // no count on the object yet
  // Ref. count is 0 at this point.
  y.DoTest(3);
  // Ref count will remain zero.
  DoIt2(y,4);
  Y.DoTest(5);
  Y.Free;
end;

Procedure Test3;

var
  Y : TTest;
begin
  Y := TTest.Create; // no count on the object yet
  // Ref. count is 0 at this point.
  y.DoTest(6);
  // Ref count will remain zero.
  DoIt1(y,7);
  y.DoTest(8);
end;


begin
  Test1;
  Test2;
  Test3;
end.

이 예시의 출력은 다음과 같아요.

Test 1 : ref count: 1
Test 2 : ref count: 2
Test 3 : ref count: 1
Destroy called
Test 3 : ref count: 0
Test 4 : ref count: 0
Test 5 : ref count: 0
Destroy called
Test 6 : ref count: 0
Test 7 : ref count: 1
Destroy called
Test 8 : ref count: 0

보시다시피 test3에서는 DoIt 호출이 끝날 때 참조 카운트가 1에서 0으로 감소해서, 호출이 반환되기 전에 인스턴스가 해제돼요.

다음 작은 프로그램은 문자열에 사용되는 참조 카운트를 보여줘요.

{$mode objfpc}
{$H+}

// Auxiliary function to extract reference count.
function SRefCount(P : Pointer) : integer;

Type
  PAnsiRec = ^TAnsiRec;
  TAnsiRec = Record
    CodePage    : TSystemCodePage;
    ElementSize : Word;
  {$ifdef CPU64}
  { align fields  }
    Dummy       : DWord;
  {$endif CPU64}
    Ref         : SizeInt;
    Len         : SizeInt;
  end;


begin
  if P=Nil then
    Result:=0
  else
    Result:=PAnsiRec(P-SizeOf(TAnsiRec))^.Ref;
end;


Procedure ByVar(Var S : string);

begin
  Writeln('By var, ref count : ',SRefCount(Pointer(S)));
end;

Procedure ByConst(Const S : string);

begin
  Writeln('Const, ref count : ',SRefCount(Pointer(S)));
end;

Procedure ByVal(S : string);

begin
  Writeln('Value, ref count : ',SRefCount(Pointer(S)));
end;

Function FunctionResult(Var S : String) : String;

begin
  Writeln('Function argument, ref count : ',SRefCount(Pointer(S)));

  Writeln('Function result, ref count : ',SRefCount(Pointer(Result)));
end;

Var
  S,T : String;

begin
  S:='Some string';
  Writeln('Constant       : ',SrefCount(Pointer(S)));
  UniqueString(S);
  Writeln('Unique         : ',SRefCount(Pointer(S)));
  T:=S;
  Writeln('After Assign   : ',SRefCount(Pointer(S)));
  ByVar(S);
  ByConst(S);
  ByVal(S);
  UniqueString(S);
  T:=FunctionResult(S);
  Writeln('After function : ',SRefCount(Pointer(S)));
end.

더 알아보기

  • Ansistring, Unicodestring, 동적 배열의 내부 구조를 살펴보면 참조 카운트를 더 잘 이해할 수 있어요.
  • UniqueString 루틴은 문자열의 참조 카운트를 강제로 1로 만들어 줄 때 써요.