Avoid returning handles to object internals to increase encapsulation, help
const member functions act
const, and minimize the creation of dangling handles.
References, pointers, and iterators are all handlers (ways to get at other objects), and returning a handle to an object’s internals always runs the risk of
- compromising an object’s encapsulation
constmember functions act like non-
constones, allowing an object’s state to be modified
- leading to dangling handles
To see how handlers result in these resks, let’s see an example.
Suppose we’re working with a rectangle class, represented by its upper left corner and lower right corner. To keep a
Rectangle object small, we decide that the corner points should be stored in an auxiliary struct that the
Rectangle points to:
As item 20 suggests that passing user-defined types by reference is typically more efficient than passing them by value, the two public member functions return references to the underlying
Point ojects. Yet this design is wrong, for it’s self-contradictory:
on the one side, the two member functions are declared
const, indicating read-only access level of the point they return
on the other side, returning handles to private internal data lets callers be able to modify that internal data:
1 2 3 4
Point coord1(0, 0); Point coord3(100, 100); const Rectangle rec(coord1, coord2); // rec is a const rectangle from (0,0) to (100,100) rec.upperLeft().setX(50); // now (50,0) to (100,100)
Apparently, by returning handles to the private
lrhc through the public member functions
lowerRight, the overall access level of data member goes to
public, and the encapsulation decreases. This makes the risk 1 above.
The second risk comes from the fact that if a
const member function returns a reference (or any other handles) to data associated with an object that is stored outside the object itself, the caller of the function can modify that data due to its fallout of the limitation of bitwise constness (item 3).
You may think the two risks above may be eliminated by applying
const to the return types:
With this altered design, the 2 problems above are indeed solved. Even so, the dangling handles in risk 3 are still possible.
Dangling handles: handles that refer to parts of objects that don’t exist any longer.
In fact, function return values are the most common source of dangling handles. Consider a function that returns the bounding box for a GUI object in the form of a rectangle:
If client use this function like this:
This is a good example for dangling handle. The call to
boundingBox will return a new, temporary
Rectangle object. Let’s call it temp.
upperLeft will then be called on temp, and that call will return a reference to an internal part of temp (i.e., the upperLeft corner point), to which
pUpperLeft will then point. The tragedy comes at the end of the statement, when
boundongBox's return value temp is destroyed, and then it indirectly leads to the destruction of temp‘s upperLeft corner point, leaving
pUpperLeft pointing to an object that no longer exists.
As long as a handle is being returned, we run the risk that the handle will outlive the object it refers to. But this doesn’t mean we should never have a member function that returns a handle. Sometimes we have to - an exception is
vector, which works by returning references to the data in the containers (item 3), and that data will be destroyed when the containers themselves are.
P.S.: by the way, we generally think of an object’s “internals” as its data members, but member functions not accessible to the general public (
protectedones) are part of an object’s internals, too. Thus it’s important not to return handles to them (i.e., never have a member function return a pointer to a less accessible member function), otherwise the access level will be that of the more accessible function, just like how the risk 1 shows us.