void func(ParamType* param) {
assert(param != nullptr);
...
}
because passing via pointer introduced undesired nullability. And if that null pointer check isn't present, or is done wrong, you've easily introduced the potential for undefined behavior into the codebase. C++ specifically gives you mutable references to express the desired behavior in these cases, and using a nullable (and memory-unsafe nullable at that) pointer is actively less clear (you now need to document the non-null precondition separately, rather than in the signature itself).Small quibble, C++ has the capability to be low level enough that the potential for UB is always there. Even with a reference, you could be dealing with a completely random chunk of memory that someone told the compiler to interpret as the data type in question.
The quibble is the presentation of this as introducing the possibility of UB (the word introduced implies it wasn't already there), instead of used as a technique for minimizing the already present potential of UB.
> Pointers can be presumed in some code bases not to be null with nullability being the unusual and documented scenario.
Just because you can train your users to assume/document when nullability is allowed, doesn't make it a better option than using a language construct that would make it explicit.
Pointers are a better option because they make the mutability apparent at the call site. Codebases with that convention work great. There is no special training or documentation overhead involved.
> C++ specifically gives you mutable references to express the desired behavior in these cases
where "these cases" refers to the common case of (a) having a mutable argument, (b) which you want passed by reference, and (c) do not want to be nullable, that is exactly the semantics achieved by passing via mutable reference. I am not claiming this is why Stroustrup added them (his own explanation for why references were added is "References are useful for several things, but the direct reason I introduced them in C++ was to support operator overloading."), but that the language provides a construct which provides exactly for the desired behavior.
> Pointers are a better option because they make the mutability apparent at the call site.
At the first call site, yes (sometimes, see below). Not at any further calls.
> Codebases with that convention work great. There is no special training or documentation overhead involved.
As someone who works with a large codebase following this style, I beg to differ. Codebases with this convention are littered with
ReturnType func(ParamType* param) {
assert(param != nullptr);
...
}
because mutable-arguments-via-pointer has introduced undesired nullability, where the intended behavior could have been expressed more directly as ReturnType func(ParamType& param) {
...
}
and, it inevitably combines all three types of methods ReturnType func1(const ParamType* param) {
if(param != nullptr) {
...
}
ReturnType func2(ParamType* param) {
assert(param != nullptr);
...
}
ReturnType func3(ParamType* param) {
if (param != nullptr) {
...
}
mingling nullability and mutability, none of which is obvious from func1(&foo);
func2(&bar);
func3(&baz);
and you still end up needing documentation to figure out what's happening.So document it regardless instead https://releases.llvm.org/3.8.0/tools/clang/docs/AttributeRe...
Hm slightly tangential but could you install some kind of page-fault handler on a reference such that the control flow jumps on write back to the last place it was accessed? Not that you’d want to do that in the real world, but it would be an interesting “pub-sub” type control flow..
There are also a number of cases where passing a pointer into a template function can be more concise. E.g. since pointers are iterators, if you have a generic function that iterates over things and you just want it to look at one thing (or zero) for testing, you can usually do this with pointer arithmetic.