Публикации FreePascal

Object Pascal: продвинутый кодинг или чему не учат в школе (Часть 1. Любите ли вы const так как люблю его я?)

20.05.2007
Золотов Алексей, http://zolotov.h14.ru

Creative Commons License

В статье мы поближе познакомимся с ключевыми словами const, var и out. При объявлении процедуры/функции на Pascal есть 4 способа передачи аргументов (в некоторых компиляторах может быть меньше). Рассмотрим все четыре способа на примере процедуры принимающей один аргумент.

Передача по значению: procedure func(S: mytype). При передаче аргумента по значению происходит копирование, то есть создается (в стеке) новая переменная и происходит копирование (как при вызове оператора присваивания). Это значит, что если mytype это массив из 1000 элементов, все эти 1000 элементов будут скопированы, на что будет потрачено время. Если mytype это ShortString, то будет скопировано до 256 байт. AnsiString и WideString внутренне представляются как указатели и казалось бы при копировании будет копироваться только SizeOf(Pointer) байт – так оно и есть, но кроме копирования будет еще неявный вызов функции, которая увеличит счетчик ссылок строки, будет добавлен неявный блок try/finally для надежного декремента счетчика ссылок (см. ниже). Тоже самое относиться и к интерфейсам, с той лишь разницей, что подсчет ссылок ведет объект, реализующий интерфейс и скорость работы завит от реализации объекта.

Остальные три типа являются вариациями передачи по ссылке:

1. procedure func(var S: mytype);
2. procedure func(const S: mytype);
3. procedure func(out S: mytype);

При передаче по ссылке функции передается не само значение, а ссылка (то бишь указатель) на переменную, содержащую передаваемое значение – поэтому вызов будет происходить быстрее (для сложных типов: строк, интерфейсов, записей, статических и динамических массивов). В случае использования var – мы можем читать и изменять значение, указываемое S, в случае использования const – только читать, случае out – только записывать.

Выбор соответствующей вариации зависит от того, что вы собираетесь делать с передаваемым значением. Если вы будете только читать, то укажите const – и пусть вас (если вы раньше программировали на С++ и любили там const) не смущает запись типа:

procedure func(const S: IList);
begin
  S.Add(‘hello world’);
end;

Ибо в Паскале, нет деления на константные и неконстантные методы (по крайней мере, пока) – здесь ключевое слово const запрещает только непосредственную модификацию указываемого объекта. Естественно, нельзя передать const аргумент другой функции, принимающий var или out аргумент.

Если вам нужно только передать значение из вызываемой функции, то используйте out аргумент:

procedure func(out S: IList);
begin
  Result := TList.Create;
end;

Неявные блоки try/finally

Кроме того, что при передаче по ссылке не происходит копирования больших блоков памяти, компилятор так же не вставляет дополнительные неявные блоки try/finally, которые нужны для гарантированного декремента ссылок (а, следовательно, для освобождения памяти и ресурсов).

Даже такая простая функция:

procedure func(S: AnsiString);
begin
end;

Порождает пустой try/finally блок – взглянем на ассемблерный листинг:

SECTION .text
	ALIGN 16
	GLOBAL P$PROGRAM_FUNC$ANSISTRING
P$PROGRAM_FUNC$ANSISTRING:
; Temps allocated between ebp-52 and ebp-4
; [12] begin
		push	ebp
		mov	ebp,esp
		sub	esp,52
; Var a located at ebp-4
		mov	dword [ebp-4],eax
		mov	eax,dword [ebp-4]
		call	NEAR FPC_ANSISTR_INCR_REF
		lea	ecx,[ebp-24]
		lea	edx,[ebp-48]
		mov	eax,1
		call	NEAR FPC_PUSHEXCEPTADDR
		call	NEAR FPC_SETJMP
		push	eax
		test	eax,eax
		jne	NEAR ..@j11
..@j11:
		call	NEAR FPC_POPADDRSTACK
; [14] end;
		lea	eax,[ebp-4]
		call	NEAR FPC_ANSISTR_DECR_REF
		pop	eax
		test	eax,eax
		je	NEAR ..@j12
		call	NEAR FPC_RERAISE
..@j12:
		leave
		ret

Жуть, не правда ли? Если переписать то же самое на псевдокоде то получиться следующее:

procedure func(S);
var A;
begin
  // создаем локальную копию 
  A := S;
  // увеличиваем счетчик ссылок для локальной копии А
  FPC_ANSISTR_INCR_REF(A);
  try
  finally
    // уменьшаем счетчик ссылок для A, то бишь освобождаем A
    FPC_ANSISTR_DECR_REF(A);
  end;
end;

Если мы передаем по ссылке, то копирования не происходит, следовательно, не вызываются FPC_ANSISTR_INCR_REF/FPC_ANSISTR_DECR_REF, следовательно, нет надобности в try/finally.

Компилятор FreePascal имеет специальную директиву {$IMPLICITEXCEPTIONS}, которая разрешает/запрещает компилятору вставлять неявные блоки try/finally. {$IMPLICITEXCEPTIONS ON} – разрешает вставлять неявную обработку исключений, а {$IMPLICITEXCEPTIONS OFF} – запрещает вставлять неявную обработку исключений. По-молчанию неявная обработка исключений разрешена и не стоит её запрещать, если вы точно не уверены в том, что делаете – грамотное использование передачи по ссылке избавит вас от неявной обработки исключений и от лишних операций копирования.

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links