It should be stressed that all identifiers other than the template placeholders should be known
when the generic class is declared. At the same time, nothing can be assumed about the template
type (unless a restriction is placed on it).
This works in several ways.
In the absence of type restrictions, the generic code cannot make assumptions about the template
type T. Consider the following unit:
unit ts;
interface
{$modeswitch advancedrecords}
type
PListEl = ^TListEl;
TListEl = packed record
Prev, Next: PListEl;
end;
implementation
type
generic LstEnumerator<T> = record
private
lst, lst_save: T;
public
constructor Create(const Value: T);
function MoveNext: boolean;
end;
function LstEnumerator.MoveNext: boolean;
begin
Result:=lst <> nil;
if Result then
lst:=lst^.next;
end;
constructor LstEnumerator.Create(const Value: T);
begin
lst:= Value;
lst_save := nil;
end;
Type
TMyListEnum = specialize LstEnumerator<TListEl>;
end.
The compiler will throw an error because when it compiles the generic definition, it cannot verify
that
lst:=lst^.next;
is correct. lst is of type T, but the compiler does not (yet) know what T is, and hence cannot know
it has a field next.
This problem can be solved with type restrictions:
unit ts;
{$mode delphi}
interface
type
TListEl = class
Prev, Next: TListEl;
end;
TMyRecord1 = Class(TListEl)
MyField : String;
end;
TMyRecord2 = Class(TListEl)
MyInteger : Integer;
end;
implementation
type
TLstEnumerator<T : TListEl> = class
private
lst, lst_save: T;
public
constructor Create(const Value: T);
function MoveNext: boolean;
end;
function TLstEnumerator<T>.MoveNext: boolean;
begin
Result:=lst <> T(nil);
if Result then
lst:=T(lst.next);
end;
constructor TLstEnumerator<t>.Create(const Value: T);
begin
lst:= Value;
lst_save := T(nil);
end;
Type
TMyRecord1Enum = TLstEnumerator<TMyRecord1>;
TMyRecord2Enum = TLstEnumerator<TMyRecord2>;
Here, the compiler knows that lst is at least of type TListEl, and hence contains members Prev
and Next.
Beside the template type, all other types used in the generic declaration must be known. This
means that a type identifier with the same name must exist. The following unit will produce an
error:
{$mode objfpc}
unit myunit;
interface
type
Generic TMyClass<T> = Class(TObject)
Procedure DoSomething(A : T; B : TSomeType);
end;
Type
TSomeType = Integer;
TSomeTypeClass = specialize TMyClass<TSomeType>;
Implementation
Procedure TMyClass.DoSomething(A : T; B : TSomeType);
begin
// Some code.
end;
end.
The above code will result in an error, because the type TSomeType is not known when the
declaration is parsed:
home: >fpc myunit.pp
myunit.pp(8,47) Error: Identifier not found "TSomeType"
myunit.pp(11,1) Fatal: There were 1 errors compiling module, stopping
A second way in which this is visible, is the following. Assume a unit
{$mode objfpc}
unit mya;
interface
type
Generic TMyClass<T> = Class(TObject)
Procedure DoSomething(A : T);
end;
Implementation
Procedure DoLocalThings;
begin
Writeln('mya.DoLocalThings');
end;
Procedure TMyClass.DoSomething(A : T);
begin
DoLocalThings;
end;
end.
The compiler will not allow to compile this unit, since the DoLocalThings function will not be
visible when the generic type is specialized:
Error: Global Generic template references static symtable
Now, if the unit is modified, and the DoLocalThings function is moved to the interface section, the
unit will compile. When using this generic in a program:
{$mode objfpc}
program myb;
uses mya;
procedure DoLocalThings;
begin
Writeln('myb.DoLocalThings');
end;
Type
TB = specialize TMyClass<Integer>;
Var
B : TB;
begin
B:=TB.Create;
B.DoSomething(1);
end.
Despite the fact that generics act as a macro which is replayed at specialization time, the reference
to DoLocalThings is resolved when TMyClass is defined, not when TB is defined. This means that
the output of the program is:
home: >fpc -S2 myb.pp
home: >myb
mya.DoLocalThings
This behavior is dictated by safety and necessity:
- A programmer specializing a class has no way of knowing which local procedures are
used, so he cannot accidentally “override” it.
- A programmer specializing a class has no way of knowing which local procedures are
used, so he cannot implement it either, since he does not know the parameters.
- If implementation procedures are used as in the example above, they cannot be
referenced from outside the unit. They could be in another unit altogether, and the
programmer has no way of knowing he should include them before specializing his class.