Am 02.11.2019 um 15:55 schrieb Ryan Joseph via fpc-pascal:
I've wanted to make a generic version of a vector for a while but I always give 
up because it seems not very possible. It's probably not even a great idea 
because many methods don't translate between float and integer but I wanted to 
prevent other code duplication if possible.

Here's an example of how things break down. Are there any solutions for this 
currently? I feel like generics need to support some compiler directives so 
different blocks of code can specialize different depending on the type.

{$mode objfpc}
{$modeswitch advancedrecords}

program generic_vector_2;
uses
   Math;

type
   generic TVec2<TScalar> = record
     x, y: TScalar;
     function Normalize: TVec2;
   end;
   TVec2f = specialize TVec2<Float>;
   TVec2i = specialize TVec2<Integer>;

function TVec2.Normalize: TVec2;
var
   fac: TScalar;
begin
   // Can't determine which overloaded function to call
   // Incompatible types: got "Extended" expected "LongInt"
   fac:=Sqrt(Sqr(x) + Sqr(y));
   if fac<>0.0 then begin
     // Incompatible types: got "Single" expected "LongInt"
     fac:=1.0/fac;
     result.x:=x*fac;
     result.y:=y*fac;
   end else begin
     result.x:=0;
     result.y:=0;
   end;
end;

begin
end.

First of Sqrt always returns a ValReal (aka the best precision floating point), thus you should declare fac as ValReal. Thus you should cast x and y to ValReal before passing them to Sqr to avoid overload troubles.

Then you only need to ensure that the adjusted vector components are passed correctly to the result which will lead to code like this:

=== code begin ===

program tgenvec;

{$mode objfpc}
{$modeswitch advancedrecords}

uses
  Math;

type
  generic TVec2<TScalar> = record
  private type
    PScalar = ^TScalar;
  public
    x, y: TScalar;
    function Normalize: TVec2;
  end;

function TVec2.Normalize: TVec2;
var
  fac, tmpx, tmpy: ValReal;
begin
  fac := Sqrt(Sqr(ValReal(x)) + Sqr(ValReal(y)));
  if fac <> 0.0 then begin
    fac := 1.0 / fac;
    tmpx := x * fac;
    tmpy := y * fac;
    if GetTypeKind(TScalar) in [tkInteger, tkInt64, tkQWord] then begin
      Result.x := Round(tmpx);
      Result.y := Round(tmpy);
    end else if GetTypeKind(TScalar) = tkFloat then begin
      Result.x := PScalar(@tmpx)^;
      Result.y := PScalar(@tmpy)^;
    end;
  end else
    Result := Default(TVec2);
end;

type
  TVec2f = specialize TVec2<Double>;
  TVec2i = specialize TVec2<LongInt>;

var
  vf: TVec2f;
  vi: TVec2i;
begin
  vf.x := 2.5;
  vf.y := 3.5;
  vf := vf.Normalize;
  Writeln(vf.x, ' ', vf.y);
  // on Win64 this prints 5.8123819371909646E-001 8.1373347120673500E-001
  vi.x := 2;
  vi.y := 4;
  vi := vi.Normalize;
  Writeln(vi.x, ' ', vi.y);
  // on Win64 this prints 0 1
end.

=== code end ===

The path not taken for the GetTypeKind inside the TVec<>.Normalize will be optimized away. The pointer conversion is needed, because a floating point type can not be assigned to an integer type. Sadly the compiler does not realize that it does not really need to take the address there, so that will stay even in O4 for the non-floating point case. But hey, you've got a generic Normalize then...

Regards,
Sven
_______________________________________________
fpc-pascal maillist  -  fpc-pascal@lists.freepascal.org
https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-pascal

Reply via email to