Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 17.  Syntax


17.1. Class Layout

This item revolves around the way we lay out class declarations. C++ implements access specification on a last-known basis, as shown in Listing 17.1.

Listing 17.1.


class ResourcePool


{


public:


  ResourcePool();                       // This is public


  ResourcePool(ResourcePool const &);   // This is public


  size_t GetCount() const;              // This is public


  Open(Rsrc *prsrc);                    // This is public


  void Close();                         // This is public


protected:


  ResourcePool(Rsrc *prsrc);            // This is protected


  virtual ~ResourcePool();              // This is protected


private:


  . . .



There's an undesirable tendency on the part of many developers[1] to clump members sharing the same access into related blocks, as shown above. The argument in favor of this policy is that the public interface of the class is in one place[2] and, therefore, readily accessible to the reader/user of the code. The kindest thing one can say about this is that it is naïve. Coding like this is a serious mistake, for both users and maintainers of the code.

[1] Even some of my august reviewers do this, or they used to, anyway!

[2] Hopefully at the start of the class! It was the fashion in the early days to put the member variables at the top of the class declaration, irrespective of their access, with the consequent results of unreadable code and practitioners of C++ who believe that member variables are more significant in class design than methods.

First, it assumes that the public interface of the class encapsulates all the important aspects of the class design. This is not so. In the example above the ResourcePool(Rsrc*) constructor is protected, and this indicates that derived classes may acquire their own Rsrc* and pass this to the base class constructor. This is an important facet of the design of ResourcePool and should not be hidden lower down in the class declaration.

A second, more prosaic reason is that this practice reduces the maintainability of the code. If you want to change the access of a member, the member has to move a significant number of lines up or down within the class declaration; such changes are hard for the reader to track in a version control system (especially a non-GUI one).

Imperfection: Abuse of C++'s last-known access specifier mechanism leads to code that is hard to use and hard to maintain.


The solution is a very simple one: order the class members in functionally related sections. Furthermore, each and every one of these sections gets its own title comment and access control specifier, even when that specifier is the same as the previous one and functionally redundant. This dramatically reduces the likelihood of introducing—admittedly trivial, but annoying nonetheless—erroneous code breaking changes to the class definition. ResourcePool should have been written as shown in Listing 17.2.

Listing 17.2.


class ResourcePool


{


// Construction


public:


  ResourcePool();


  ResourcePool(ResourcePool const &);


protected:


  ResourcePool(Rsrc *prsrc);


  virtual ~ResourcePool();


// Operations


public:


  void Open(Rsrc *prsrc);


  void Close();


// Attributes


public:


  size_t GetCount() const;


private:


  . . .



When we look at this class declaration, it is clear which operations are involved in the construction of the class, which are its operations, and so on. Rather than have the destructor and the third constructor skulking down at the end of the class definition with the member variables, they are collected together with the other construction members. In real code, the number of functional sections will include some or all of Construction, Operations, Attributes, Iteration, State, Implementation, Members, and my favorite, Not to be implemented. This last is the section, which I place at the very end of a class definition, that contains the items that are deliberately inaccessible (and undefined). We talked in Chapter 2 about the various ways to control the clients of one's class, such as hiding copy assignment operators, and these go in this section, as in:



  . . .


// Not to be implemented


private:


  ResourcePool &operator =(ResourcePool const &);


};



In fact, this violates my rule about all logically related parts being grouped together, and you would be quite right to point that out. I prefer it because there is a single place where all things banished reside, kind of like a method purgatory. However, I acknowledge the inconsistency, and you may choose to do differently in your own code.

Let's look at the benefit of this new approach. Suppose that we want to change the access of ~ResourcePool() so that ResourcePool hierarchy instances may be destroyed polymorphically. This involves the simple insertion of public: before the destructor, and protected: after it in order to retain the previous access for the remainder of the block (see Listing 17.3).

Listing 17.3.


class ResourcePool


{


// Construction


public:


  ResourcePool();


  ResourcePool(ResourcePool const &);


protected:


  ResourcePool(Rsrc *prsrc);


public:


  virtual ~ResourcePool();


protected:


  . . .



It's now obvious what has happened, and why, when you look at the differences in a version control system. For readers of the code, the class structure is unchanged. The construction section still contains a list of constructors, followed by a single destructor. It is clear which are publicly accessible, and which are not, providing substantial self-documenting information. Remember: the only documentation that is guaranteed to be up-to-date is the code [Dewh2003, Kern1999].[3]

[3] A couple of friends, whose work I mostly respect, and occasionally ape, take this to the extreme. For them "the code is the documentation!"

Finally, I'd like to point out the obvious advantage of this approach. Because one quickly gets into the habit of having a single consistent structure, one is far less likely to forget to do the housekeeping tasks (e.g., ensuring that copy constructors and copy assignment operators are supplied or proscribed for types that manage resources; see Chapters 3 and 4) that good C++ practice dictates. Furthermore, writing parser scripts (in Perl, Python, or Ruby) to manipulate the code is much simplified.

A word of caution: if you find yourself persuaded by this, the very last thing you should do is go and apply it to all the code in your code base. You'll make history tracking of the existing code base virtually impossible, and your coworkers will murder you in your sleep! Always save wholesale application of any improved layout schemes to new code, and make gentle increments to extant code.


      Previous section   Next section