In many situations, a Foo*&
is a replacement for a Foo**
. In both cases, you have a pointer whose address can be modified.
Suppose you have an abstract non-value type and you need to return it, but the return value is taken up by the error code:
error_code get_foo( Foo** ppfoo )
or
error_code get_foo( Foo*& pfoo_out )
Now a function argument being mutable is rarely useful, so the ability to change where the outermost pointer ppFoo
points at is rarely useful. However, a pointer is nullable -- so if get_foo
's argument is optional, a pointer acts like an optional reference.
In this case, the return value is a raw pointer. If it returns an owned resource, it should usually be instead a std::unique_ptr<Foo>*
-- a smart pointer at that level of indirection.
If instead, it is returning a pointer to something it does not share ownership of, then a raw pointer makes more sense.
There are other uses for Foo**
besides these "crude out parameters". If you have a polymorphic non-value type, non-owning handles are Foo*
, and the same reason why you'd want to have an int*
you would want to have a Foo**
.
Which then leads you to ask "why do you want an int*
?" In modern C++ int*
is a non-owning nullable mutable reference to an int
. It behaves better when stored in a struct
than a reference does (references in structs generate confusing semantics around assignment and copy, especially if mixed with non-references).
You could sometimes replace int*
with std::reference_wrapper<int>
, well std::optional<std::reference_wrapper<int>>
, but note that is going to be 2x as large as a simple int*
.
So there are legitimate reasons to use int*
. Once you have that, you can legitimately use Foo**
when you want a pointer to a non-value type. You can even get to int**
by having a contiguous array of int*
s you want to operate on.
Legitimately getting to three-star programmer gets harder. Now you need a legitimate reason to (say) want to pass a Foo**
by indirection. Usually long before you reach that point, you should have considered abstracting and/or simplifying your code structure.
All of this ignores the most common reason; interacting with C APIs. C doesn't have unique_ptr
, it doesn't have span
. It tends to use primitive types instead of structs because structs require awkward function based access (no operator overloading).
So when C++ interacts with C, you sometimes get 0-3 more *
s than the equivalent C++ code would.