Discussion
Team LiB
Previous Section Next Section

Discussion

Consider:



class Socket {


public:


  // … constructor that opens handle_, destructor that closes handle_, etc. …


  int GetHandle() const {return handle_;}     // avoid this


private:


  int handle_;                                   // perhaps an OS resource handle


};



Data hiding is a powerful abstraction and modularity device (see Items 11 and 41). But hiding data and then giving away handles to it is self-defeating, just like locking your house and leaving the keys in the lock. This is because:

  • Clients now have two ways to implement functionality: They can use your class's abstraction (Socket) or directly manipulate the implementation that your class relies on (the socket's C-style handle). In the latter case, the object is unaware of significant changes to the resource it thinks it owns. Now the class cannot reliably enrich or embellish functionality (e.g., proxying, logging, collecting statistics) because clients can bypass the embellished, controlled implementationand any of the invariants it thinks it's adding, which makes correct error handling next to impossible (see Item 70).

  • The class cannot change the underlying implementation of its abstraction because clients depend on it: If Socket is later upgraded to support a different protocol with a different set of low-level primitives, calling code that fetches the underlying handle_ and manipulates it incorrectly will be silently broken.

  • The class cannot enforce its invariants because calling code can alter state unbeknownst to the class: For example, someone could close the handle being used by a Socket object without going through a Socket member function, thus rendering the object invalid.

  • Client code can store the handles that your class returns, and attempt to use them after your class's code has invalidated them.

A common mistake is to forget that const is shallow and doesn't propagate through pointers (see Item 15). For example, Socket::GetHandle is a const member; as far as the compiler is concerned, returning handle_ preserves constness just fine. However, raw calls to system functions using handle_'s value can certainly modify data that handle_ refers to indirectly.

The following pointer example is similar, although we'll see that the situation is slightly better because at least a const return type can reduce accidental misuses:



class String {


  char* buffer_;


public:


  char* GetBuffer() const {return buffer_;}  // bad: should return const char*


  // …


};



Even though GetBuffer is const, this code is technically valid and legal. Clearly, a client can use this GetBuffer to change a String object in quite major ways without explicit casting and therefore accidentally; for example, strcpy( s.GetBuffer(), "Very Long String…" ) is legal code; in practice, every compiler we tried compiles it without a warning. Returning const char* instead from this member function would at least cause a compile-time error for such misuses so they could not occur accidentally; such calling code would have to write an explicit cast (see Items 92 to 95).

Even returning pointers to const does not eliminate all accidental misuses, because another problem with giving away object internals has to do with the internals' validity. In the above String example, calling code might store the pointer returned by GetBuffer, then perform an operation that causes the String to grow (and move) its buffer, and finally (and apocalyptically) try to use the saved-and-now-invalidated dangling pointer to a buffer that no longer exists. Thus, if you do think you have a good reason to yield such internal state, you must still document in detail how long the returned value remains valid and what operations will invalidate it (compare this with the standard library's explicit iterator validity guarantees; see [C++03]).

    Team LiB
    Previous Section Next Section