Most of the answers here fail to address the inherent ambiguity in having a raw pointer in a function signature, in terms of expressing intent. The problems are the following:
The caller does not know whether the pointer points to a single objects, or to the start of an "array" of objects.
The caller does not know whether the pointer "owns" the memory it points to. IE, whether or not the function should free up the memory. (foo(new int)
- Is this a memory leak?).
The caller does not know whether or not nullptr
can be safely passed into the function.
All of these problems are solved by references:
References always refer to a single object.
References never own the memory they refer to, they are merely a view into memory.
References can't be null.
This makes references a much better candidate for general use. However, references aren't perfect - there are a couple of major problems to consider.
- No explicit indirection. This is not a problem with a raw pointer, as we have to use the
&
operator to show that we are indeed passing a pointer. For example, int a = 5; foo(a);
It is not clear at all here that a is being passed by reference and could be modified.
- Nullability. This weakness of pointers can also be a strength, when we actually want our references to be nullable. Seeing as
std::optional<T&>
isn't valid (for good reasons), pointers give us that nullability you want.
So it seems that when we want a nullable reference with explicit indirection, we should reach for a T*
right? Wrong!
Abstractions
In our desperation for nullability, we may reach for T*
, and simply ignore all of the shortcomings and semantic ambiguity listed earlier. Instead, we should reach for what C++ does best: an abstraction. If we simply write a class that wraps around a pointer, we gain the expressiveness, as well as the nullability and explicit indirection.
template <typename T>
struct optional_ref {
optional_ref() : ptr(nullptr) {}
optional_ref(T* t) : ptr(t) {}
optional_ref(std::nullptr_t) : ptr(nullptr) {}
T& get() const {
return *ptr;
}
explicit operator bool() const {
return bool(ptr);
}
private:
T* ptr;
};
This is the most simple interface I could come up with, but it does the job effectively.
It allows for initializing the reference, checking whether a value exists and accessing the value. We can use it like so:
void foo(optional_ref<int> x) {
if (x) {
auto y = x.get();
// use y here
}
}
int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability
We have acheived our goals! Let's now see the benefits, in comparison to the raw pointer.
- The interface shows clearly that the reference should only refer to one object.
- Clearly it does not own the memory it refers to, as it has no user defined destructor and no method to delete the memory.
- The caller knows
nullptr
can be passed in, since the function author explicitly is asking for an optional_ref
We could make the interface more complex from here, such as adding equality operators, a monadic get_or
and map
interface, a method that gets the value or throws an exception, constexpr
support. That can be done by you.
In conclusion, instead of using raw pointers, reason about what those pointers actually mean in your code, and either leverage a standard library abstraction or write your own. This will improve your code significantly.
new
to create a pointer and the resulting issues of ownership. – Martin York