Wednesday, August 26, 2009

Const and Containment, or Do as the Vectors Do

What does it mean to be const-correct for an accessor? It's not uncommon to see an accessor method declared const that returns a mutable object stored in a member container:
class World
{
public:
    GameObject * getGameObjectAt(int index) const
    {
        return m_gameObjects[index];
    }
private:
    std::vector  m_gameObjects;
};
For the duration of the call to getGameObjectAt(), the class remains bit-wise const. Nothing is actually changed in the call. The same rationale is applied to the container, returning the element doesn't actually change the container for the lifetime of the call. The caller is getting a copy of the pointer in the container. The copy, however, is a non-const pointer, meaning the value pointed to can be changed! This can't happen with iterators or references.

What happens if GameObject contains methods that mutate World?
class GameObject
{
public:
    void doCombat()
    {
        // ... do combat-y things
        if(target->isDead())
        {
            m_world->removeGameObject(target);
        }
    }
};
This brief, contrived example keeps it pretty clear to a programmer that getting a GameObject from World and invoking doCombat() may change the World object and its m_gameObjects container. In the real-world, game code is rarely so brief or simple. Dozens (or more) programmers are working with volumes of code with very complex and subtle interactions.

A pretty common response from some programmers is "don't do that". This isn't helpful if the author of the code isn't around to berate the offending programmer for shooting himself in the foot. A good API will communicate "don't do that" through the interface and let the compiler yell at people for doing the wrong thing.

Taking the extra step to cross the line between bit-wise const-correct code and logically const code is how this is accomplished. Do as the vectors do when accessing aggregate data stored in containers. Use const overloading for accessors.
template
class Container
{
public:
    const Element & operator[] (size_t index) const;
    Element & operator[] (size_t index);
};
A logically const World class would look like:
class World
{
public:
    const GameObject * getGameObjectAt(int index) const
    {
        return m_gameObjects[index];
    }
    GameObject * getGameObjectAt(int index)
    {
        return m_gameObjects[index];
    }
private:
    std::vector  m_gameObjects;
};
While providing a const and non-const interface seems redundant and verbose, it does enforce the World class designer's intent that users of World not change it in a const context. If they try it, the compiler will effectively say "don't do that", and mis-use becomes more difficult (though not impossible with const_cast or C-Style casting). At that point, however, the user of a const World object has to jump through some hoops and explicitly say "I know what I am doing here, what I am doing is safe and correct!"

Const-correct code is safer code, but don't stop at bit-wise const to pacify a squawking compiler, take it all the way to logically const correct code to turn the pesky compiler on programmers about to do something unsafe.

No comments:

Post a Comment