Pointer Implications (C)

By | 2013-08-05

Consider the declarations of various pointer-to-an-O-accepting functions:

  • f( O * )
  • f( const O * )
  • f( O * const )
  • f( const O * const )

What is the author of f() telling us about what the function will do with the O we reference in each of these cases? This, and subsequent articles, will discuss why you would choose between these various argument types. Essentially we’re talking about what you are implying to the caller of your function when you pick a particular pointer declaration.

f(O*)

We’re passing a pointer by value, it’s so common that it’s easy to forget that passing a pointer, while it’s a pass-by-reference to the pointed to object, is still pass by value as far as the parameter is concerned. It should be obvious that this does nothing:

    void f(O* p) {
        p = nullptr;
    }

p has been passed-by-value, so is just another local variable. *p on the other hand is a reference to some external object and we certainly can modify that inside f().

    void f(O* p) {
        if(p == nullptr)
            return;
        p->modify();
    }

The above is, fundamentally, what the function declaration f(O*) says about f() – it will modify the object given to it, and the object need not exist (passing a pointer means you have to allow that the pointer points at nothing, null). There is, unfortunately, nothing to stop f() modifying not just the content, but the lifetime of O.

    void f(O* p) {
        delete p;
        // The following has no effect, we've established that the
        // p has been passed by value
        p = nullptr;
    }

    // ... elsewhere in the forest ...
    myObject = new O;
    f( myObject );
    // f() has destroyed myObject, so we have to remember to do this
    myObject = nullptr;
    // ... if we forget then when we try this, we could be hiding a
    // disaster for later.
    myObject->modify();

We’ll come to the correct way to modify an object lifetime within a function later. For now, here’s a rule: never delete an object via a raw pointer passed-by-value to a function. Let’s look at why:

    void f(O* p) {
        delete p;
        // The following has no effect, we've established that the
        // p has been passed by value
        p = nullptr;
    }

    // ... elsewhere in the forest ...
    O myObject;
    // This will go very, very badly, as f() assumes that the passed
    // object is allocated on the free-store.  It isn't.
    f( &myObject );

By using delete in a function, we’ve forced all objects of that type to be allocated on the free-store; and we’ve forced the caller to come and read our API in order to know that fact. We should, as far as possible, not inform of limitations by comment. We should limit by language, and let the compiler give errors when those limits aren’t obeyed. Unfortunately, C offers no such facility.

f(const O*)

One standard limit, is that of const. Here we are telling the caller that f() will not modify the pointed-to object, and that the object is allowed not to exist. The compiler will warn us if we try to use any non-const member function of O.

    int f(const O* p) {
        if(p == nullptr)
            return DEFAULT;
        return p->query();
    }

A function that takes a const O* would be pointless if it didn’t return something. Since const O* can’t be modified, if f() doesn’t return something, then it would have zero effect. Here we’re using f() as a null-safe wrapper around an O.

We needn’t worry about lifetime in this case, delete requires a non-const pointer, so the compiler will not let us call delete p here.

It’s important to remember what const O* means – that the pointed-to object cannot be modified. The pointer itself can be modified. As in the previous section though, it would serve no purpose. However, the compiler will not stop you.

    int f(const O* p) {
        if(p == nullptr)
            return DEFAULT;
        int ret = p->query();
        p = nullptr;
        return ret;
    }

f(O * const) and f(const O * const)

I’ve included this one for completeness; but you will never see it. This is a nonsense declaration. O * const is our way of telling the compiler that the pointer is itself const and cannot be modified. The thing pointed to is fair game. Still remembering that the pointer is passed by value, we are talking only about the function-scoped copy of the pointer that can’t be modified. We’ve already seen that modifying the pointer within the function does absolutely nothing, so why would it matter to the caller that we’ve told the compiler to prevent us from doing that? From the perspective of the caller these two declarations are identical:

  • f( O * )
  • f( O * const )

The caller doesn’t care whether the function modifies it’s local copy of the pointer or not, it only cares that f() can modify the pointed-to object, which in both these cases it can.

By extension then, the following two declarations are identical from the point of view of the caller.

  • f( const O * )
  • f( const O * const )

The caller knows that neither modifies the pointed-to-object.


Next time, C++.

Leave a Reply