4

I want to check to see if a generic type is "valid" without using constraints.

In other words, I want to write the following code:

class function TMaybe<T>.FromValue(aValue: T): TMaybe<T>;
begin
  if T <> nil then
  begin
    Result := TMaybe<T>.Some(aValue);
  end else
  begin
    Result := TMaybe<T>.None;
  end;
end;

However, this doesn't compile with the error:

E2571 Type parameter 'T' doesn't have class or interface constraint

Obviously for a class like this, I'd like to be able to have any type be a TMaybe.

Is there a way to check to see if contraint-less type is "valid", that is, not null? (I don't care about empty strings, etc.)

Should I write a TypeIsEmpty<T>(aValue: T): Boolean that uses TypInfo to figure it out? I'd like to avoid that.

Nick Hodges
  • 16,902
  • 11
  • 68
  • 130
  • 1
    Any specific concern why you want to avoid using TypInfo? – Ken Bourassa Jan 24 '20 at 21:58
  • 1
    Would compiler intrinsic routine [GetTypeKind](https://delphisorcery.blogspot.com/2014/10/new-language-feature-in-xe7.html?m=1) help? – Peter Wolf Jan 24 '20 at 23:11
  • Read this post by @RudyVelthuis, bless his soul: [The current state of generics in Delphi](http://rvelthuis.blogspot.com/2018/10/the-current-state-of-generics-in-delphi.html). Also [Undocumented intrinsic routines](https://stackoverflow.com/a/30417597/576719) – LU RD Jan 24 '20 at 23:24
  • @LURD: Oh, God. I had no idea. He will be missed. – Andreas Rejbrand Jan 26 '20 at 15:12
  • @KenBourassa I avoid RTTI in general for performance reasons. But of course, sometimes, as here, it is the only way. – Nick Hodges Jan 28 '20 at 13:54

1 Answers1

5

Try something like this:

type
  PMethod = ^TMethod;

class function TMaybe<T>.FromValue(aValue: T): TMaybe<T>;
begin
  {
  the *undocumented* IsManagedType() intrinsic function returns True if T
  is an interface, string, or dynamic array, but it also returns True if T
  is a record containing such a field. Since a record can't be compared to
  nil, IsManagedType(T) is not useful here.

  Using the *undocumented* GetTypeKind() intrinsic function can be used
  instead to handle ONLY nil-able types...
  }
  // if IsManagedType(T) then...
  case GetTypeKind(T) of
    tkString, tkClass, tkLString, tkWString, tkInterface, tkDynArray, tkUString:
    begin
      if PPointer(@aValue)^ = nil then
        Exit(TMaybe<T>.None);
    end;
    tkMethod:
    begin
      if (PMethod(@aValue)^.Data = nil) or (PMethod(@aValue)^.Code = nil) then
        Exit(TMaybe<T>.None);
    end;
  end;
  Exit(TMaybe<T>.Some(aValue));
end;

Intrinsic functions like GetTypeKind() (and IsManagedType()) are evaluated at compile-time, and as such code branches that evaluate to False are optimized out of the final executable. However, as @DavidHeffernan mentions in comments, the compiler checks the code syntax before it instantiates the generic, hence the PPointer typecast to get around that.

So, if you set T to a nil-able type, like a String, the compiler will be able to optimize the code down to this:

class function TMaybe<String>.FromValue(aValue: String): TMaybe<String>;
begin
  if Pointer(aValue) = nil then
    Exit(TMaybe<String>.None);
  Exit(TMaybe<String>.Some(aValue));
end;

And if you set T to a non nil-able type, like an Integer, the compiler will be able to optimize the code down to this instead:

class function TMaybe<Integer>.FromValue(aValue: Integer): TMaybe<Integer>;
begin
  Exit(TMaybe<Integer>.Some(aValue));
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • `if aValue = nil then` does not compile with E2015. I guess you could fix it by some pointer casts in your case statement. Like `if PPointer(@aValue)^ = nil then`. You'd need a different branch for double pointer methods. Where you reasoning went wrong is that the compiler syntax checks the code before it instantiates the generic. Your reasoning would be fine if these were templates. – David Heffernan Jan 25 '20 at 08:20
  • Also, the string types are in the wrong place. They can't be `nil` at a language level. For sure the empty string is implemented that way, but since you can't write `if s = nil` then they belong in the other branch. But that's not really the main point, Nick can decide the rules of the game. – David Heffernan Jan 25 '20 at 08:23
  • @DavidHeffernan strings are implemented as pointers, so the `PPointer` typecast will work for them as well, no need for another code branch to handle them separately. But `TMethod` is a little different, though. I updated my answer. – Remy Lebeau Jan 25 '20 at 16:39
  • the code compiles and runs for string. I didn't say otherwise. It is just logically wrong. Because there is no null value for strings. – David Heffernan Jan 25 '20 at 16:47
  • 1
    Brilliant -- thanks, Remy. I was hoping there was a slightly cleaner way, but this is excellent. Thanks. – Nick Hodges Jan 25 '20 at 17:06