Creating a NotNull struct

By disabling default construction, we can force initialization of a type. Using this feature, we'll make an object reference that will force the user to check for null before using it.

How to do it…

To create a NotNull struct, perform the following steps:

  1. Create struct NotNull(T) with a private member of type T.
  2. Disable default construction with @disable this();.
  3. Write a private constructor that takes T and assert(t !is null);.
  4. Write a property function that returns the payload and use it with alias this.
  5. Write a helper struct CheckNull(T), with a member T. The member T is a property that returns NotNull!T and has opCast(T : bool), which checks the payload for null and returns true if it is not null.
  6. Write a helper function, checkNull, which takes a T member and returns CheckNull!T.
  7. Write your functions to accept and return NotNull!T whenever possible.
  8. Optionally, write a generic factory function that returns NotNull!T.
  9. Use the function with if(auto nn = test.checkNull) {}.

The following is the code:

struct NotNull(T) {
  private T notNullPayload;
  @disable this();
  private this(T t) {
    assert(t !is null);
    notNullPayload = t;
  }
  inout(T) getNotNullPayload() inout {
    return notNullPayload;
  }
  alias getNotNullPayload this;
}

struct CheckNull(T) {
  private T payload;
  bool opCast(T : bool)() { return payload !is null; }
  inout(NotNull!T) getNotNullPayload() inout {
    return NotNull!T(cast(T) payload);
  }
  alias getNotNullPayload this;
}
CheckNull!T checkNull(T)(T t) {
  return CheckNull!T(t);
}

// test usage in a function
void test(NotNull!(int*) nn) {
  import std.stdio;
  writeln(*nn, " was not null");
}

void main() {
  import std.stdio;
  int item;
  int* ptr = &item;

  // how to do the test, also try if ptr is null
  if(auto nn = ptr.checkNull) {
    test(nn);
  } else {
    writeln("Null");
  }
}

How it works…

There are three key points in this implementation: alias this, which allows us to control implicit conversions; the disabled default constructor, which forces initialization of any NotNull member or local variable; and the helper type with opCast(T:bool), which enables the type transformation inside the if statement.

We use a getter property with alias this instead of going directly to the member, because the getter property prohibits assignment. If we used alias this directly with the property, it would allow the assigning of null:

NotNull!Object o = …;
o = null; // this would be allowed because alias this would rewrite it to o.payload = null, bypassing our check!

The getter property uses inout to provide a start to const-correctness.

We use @disable this(); to force initialization of the object. The syntax for a default constructor is this(). While D's structs cannot have default constructors, the language nevertheless lets you write a prototype specifically to disable it. All variables in D are initialized to default values upon declaration (unless you specifically skip initialization with =void at the declaration point). The default value of a reference is null. Thus, to ensure that a NotNull structure is never null, we must force explicit initialization. The language will require an initializer at any local variable declaration and an initializer in the constructor if NotNull is used as an object member.

Moreover, we made the constructor private, forcing the initialization process to go through our helper functions and type. This gives us control similar to languages with special nullable types built into the language. It forces a check and gives us an opportunity to limit the scope of the not null type to the correct if branch.

Next, the CheckNull type and the helper constructor serves as an intermediate between normal, nullable types and the NotNull type, with its guarantees. On using alias this, it allows implicit conversion to NotNull upon request (which then performs an additional check for null at assignment time via assert in non-release mode). However, first, the opCast allows it to be checked inside an if statement.

The compiler converts if(a) to if(cast(bool) a). The opCast method gives us control over explicit casting to any type, which is given as a compile-time parameter. Here, we specialize on the argument of bool since that is the only operation we care about with this type. It returns true if the object is not null. Then, using a variable declaration inside the if statement, we assign the new type to a local variable and can then use it or pass it to functions as NotNull!T. This, in turn, implicitly converts back to the original type T, allowing easy access to all the original type's methods and operations. A not null reference is always substitutable for a possibly null reference.

Note

The CheckNull type is necessary in addition to the NotNull type because CheckNull might be null, whereas NotNull is guaranteed not to be. Adding opCast to NotNull is tempting to simplify the code, but then we'd have to remove the assertions for the if branching to work, thereby breaking the not-null invariant. It would let you write NotNull!T test = checkNull(a_null_reference) without compile-time and runtime null checking.

It is possible to write Object o = null; NotNull!Object obj = checkNull(o);. This will compile despite the lack of an if statement to test null, triggering a runtime assertion failure. This is the best we can do with a library type, but note that this is easy to catch upon code review and it is still an improvement over not using NotNull at all, since it will trigger the error immediately at assignment time instead of some indeterminate point later in the code when the reference is finally used. The difficult part of debugging null dereference errors is often figuring out why the value was null in the first place. The assertion failure at assignment time will significantly narrow down the possibilities.

There's more…

A const-correctness implementation of NotNull is in Phobos' std.typecons module. The FIXME comment is not true right now, but should be by the time this is published. Confirm that is the case.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.74.211