Don't use either if you can avoid it. Use a factory function returning an option type, a tuple, or a dedicated sum type. In other words, represent a potentially-defaulted value with a different type from a guaranteed-not-to-be-defaulted value.
First, let's list the desiderata then think about how we can express this in a few languages (C++, OCaml, Python)
- catch unwanted use of a default object at compile time.
- make it obvious whether a given value is potentially-defaulted or not while reading the code.
- choose our sensible default value, if applicable, once per type. Definitely don't pick a potentially different default at every call site.
- make it easy for static analysis tools or a human with
grep
to hunt for potential mistakes.
- For some applications, the program should continue normally if unexpectedly given a default value. For other applications, the program should halt immediately if given a default value, ideally in an informative way.
I think the tension between the Null Object Pattern and the null pointers comes from (5). If we can detect errors early enough, though, (5) becomes moot.
Let's consider this language by language:
C++
In my opinion, a C++ class should generally be default-constructible since it eases interactions with libraries and makes it easier to use the class in containers. It also simplifies inheritance since you don't need to think about which superclass constructor to call.
However, this means that you can't know for sure whether a value of type MyClass
is in the "default state" or not. Besides putting in a bool nonempty
or similar field to surface default-ness at runtime, the best you can do is produce new instances of MyClass
in a way that compels the user to check it.
I'd recommend using a factory function that returns a std::optional<MyClass>
, std::pair<bool, MyClass>
or an r-value reference to a std::unique_ptr<MyClass>
if possible.
If you want your factory function to return some kind of "placeholder" state that's distinct from a default-constructed MyClass
, use a std::pair
and be sure to document that your function does this.
If the factory function has a unique name, it's easy to grep
for the name and look for sloppiness. However, it is hard to grep
for cases for where the programmer should have used the factory function but didn't.
OCaml
If you are using a language like OCaml, then you can just use an option
type (in the OCaml standard library) or an either
type (not in the standard library, but easy to roll your own). Or a defaultable
type (I'm making up the term defaultable
).
type 'a option =
| None
| Some of 'a
type ('a, 'b) either =
| Left of 'a
| Right of 'b
type 'a defaultable =
| Default of 'a
| Real of 'a
A defaultable
as shown above is better than a pair because the user must pattern match to extract the 'a
and can't simply ignore the first element of the pair.
The equivalent of the defaultable
type as shown above can be used in C++ using a std::variant
with two instances of the same type, but parts of the std::variant
API are unusable if both types are the same. Also it's a weird use of std::variant
since its type constructors are unnamed.
Python
You don't get any compile-time checking anyway for Python. But, being dynamically typed, there generally aren't circumstances where you need a placeholder instance of some type to placate the compiler.
I would recommend just throwing an exception when you would be forced to create a default instance.
If that's not acceptable, I would recommend creating a DefaultedMyClass
that inherits from MyClass
and returning that from your factory function. That buys you flexibility in terms of "stubbing out" functionality in defaulted instances if you need to.