Discussionclass 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:
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]). |