If I have a find
function that can sometimes fail to find the required thing, I tend to make that function return a pointer such that a nullptr
indicates that the thing was not found.
E.g.
Student* SomeClass::findStudent(/** some criteria. */)
If the Student exists, it will return a pointer to the found Student
object, otherwise it will return nullptr
.
I've seen boost::optional
advocated for this purpose as well. E.g. When to use boost::optional and when to use std::unique_ptr in cases when you want to implement a function that can return "nothing"?
My question is, isn't returning a pointer the best solution in this case. i.e. There is a possibility that the queried item will not be found, in which case returning nullptr is a perfect solution. What is the advantage of using something like boost::optional
(or any other similar solution)?
Note that, in my example, findStudent
will only ever return a pointer to an object that is owned by SomeClass
.
optional<T&>
was removed from the C++ standardization track because its use is questionable: it behaves nearly identically to a non-owning T*
with slightly different (and confusingly different from optional<T>
and T*
) semantics.
optional<T&>
is basically a non-owning T*
wrapped up pretty, and somewhat strangely.
Now, optional<T>
is a different beast.
I have used optional<Iterator>
in my container-based find algorithms. Instead of returning end()
, I return the empty optional. This lets users determine without a comparison if they have failed to find the item, and lets code like:
if(linear_search_for( vec, item))
work, while the same algorithm also lets you get at both the item and the location of the item in the container if you actually need it.
Pointers to elements doesn't give you the location information you might want except with contiguous containers.
So here, I've created a nullable iterator that has the advantages of iterators (generically working with different types of containers) and pointers (can be tested for the null state).
The next use is actually returning a value. Suppose you have a function that calculates a rectangle.
Rect GetRect();
now, this is great. But what if the question can be meaningless? Well, one approach is to return an empty rect or other "flag" value.
Optional lets you communicate that it can return a rect, or nothing, and not use the empty rect for the "nothing" state. It makes the return value nullable.
int GetValue();
is a better example. An invalid value could use a flag state of the int -- say -1
-- but that forces every user of your function to look up and track the flag state, and not accidentally treat it as a normal state.
Instead, optional<int> GetValue()
makes it clear that it can fail, and what the failure state it. If it is populated, you know it is a real value, and not a flag value.
In both of these cases, returning a non-owning pointer is non-viable, because who owns the storage? Returning an owning pointer is expensive, because pointless heap allocations are pointless.
Optionals are nullable value types. When you want to manage resources locally, and you still want an empty state, they make it clear.
Another thing to look into is the expected
type being proposed. This is an optional, but when in the empty state contains a reason why it is empty.
The advantage of an optional<Student&>
return type here is that the semantics of usage are readily apparent to all users that are familiar with optional
(and will become readily apparent once they familiarize themselves with it). Those semantics are:
Student
and is not responsible for memory management. The caller simply gets a reference to an existing object.optional<T>
is self-documenting in a way that T*
isn't. Moreover, it has other benefits in that it can work in cases where you want to return any kind of object type without the need for allocation. What if you needed to return an int
or double
or SomePOD
?
Lets consider you have a std::map<IndexType, ValueType>
where you are trying to find something (Note: The same applies for other containers, this is just to have an example). You have these options:
ValueType&
: The user can modify your map-content and does not need to think about memory-allocation/deallocation. But if you dont find anything in your map, you need to throw an exception or something similar.ValueType*
: The user can modify your map-content and you can return a nullptr if you dont find anything. But the user can call delete on that pointer and you must specify anyhow if he has to do so or not.ValueType
: The user does not have to worry about delete or not-delete and can modify your map-content depending on the type of smart-pointer. You can also return a nullptr. But this pretty much requires you to deal with smart_pointers in your map, which is overly complicated if ValueType
would be e.g. just an int otherwise.ValueType
: The user can not modify your map-content and does not need to think about memory-allocation/deallocation. But if you dont find anything in your map, you need to return some special ValueType
which tells the user you didn't find anything. In case your ValueType
is e.g. int
, which one would you return that makes clear "no int found".ValueType
return by value with the additional option of "not returning a ValueType
"optional<T&>
may indeed be replaced by T*
but T*
has not clear semantic (ownership ?).
But optional<T>
cannot be replaced by T*
.
For example:
optional<Interval> ComputeOverlap(const Interval&, const Interval&);
If there is no overlap, no problem with T*
(nullptr
) or optional<T>
.
But if there is an overlap, we need to create a new interval. We may return a smart_pointer in this case, or optional.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With