3
Coding with Style

If you’re going to spend several hours each day in front of a keyboard writing code, you should take some pride in all that work. Writing code that gets the job done is only part of a programmer’s work. After all, anybody can learn the fundamentals of coding. It takes a true master to code with style.

This chapter explores the question of what makes good code. Along the way, you’ll see several approaches to C++ style. As you will discover, simply changing the style of code can make it appear very different. For example, C++ code written by Windows programmers often has its own style, using Windows conventions. It almost looks like a completely different language than C++ code written by Mac OS programmers. Exposure to several different styles will help you avoid that sinking feeling you get when opening up a C++ source file that barely resembles the C++ you thought you knew.

THE IMPORTANCE OF LOOKING GOOD

Writing code that is stylistically “good” takes time. You probably don’t need much time to whip together a quick-and-dirty program to parse an XML file. Writing the same program with functional decomposition, adequate comments, and a clean structure would take you more time. Is it really worth it?

Thinking Ahead

How confident would you be in your code if a new programmer had to work with it a year from now? A friend of mine, faced with a growing mess of web application code, encouraged his team to think about a hypothetical intern who would be starting in a year. How would this poor intern ever get up to speed on the code base when there was no documentation and scary multiple-page functions? When you’re writing code, imagine that somebody new or even you will have to maintain it in the future. Will you even still remember how it works? What if you’re not available to help? Well-written code avoids these problems because it is easy to read and understand.

Elements of Good Style

It is difficult to enumerate the characteristics of code that make it “stylistically good.” Over time, you’ll find styles that you like and notice useful techniques in code that others wrote. Perhaps more important, you’ll encounter horrible code that teaches you what to avoid. However, good code shares several universal tenets that are explored in this chapter:

  • Documentation
  • Decomposition
  • Naming
  • Use of the language
  • Formatting

DOCUMENTING YOUR CODE

In the programming context, documentation usually refers to comments contained in the source files. Comments are your opportunity to tell the world what was going through your head when you wrote the accompanying code. They are a place to say anything that isn’t obvious from looking at the code itself.

Reasons to Write Comments

It may seem obvious that writing comments is a good idea, but have you ever stopped to think about why you need to comment your code? Sometimes programmers acknowledge the importance of commenting without fully understanding why comments are important. There are several reasons, all of which are explored in this chapter.

Commenting to Explain Usage

One reason to use comments is to explain how clients should interact with the code. Normally, a developer should be able to understand what a function does simply based on the name of the function, the type of the return value, and the name and type of its parameters. However, not everything can be expressed in code. Sometimes a function requires certain pre- or postconditions that you have to explain in a comment. Exceptions that can be thrown by a function are also something that should be explained in a comment. In my opinion, you should only add a comment if it really adds any useful information. So, it’s up to the developer to decide whether a function needs a comment or not. Experienced programmers will have no problems deciding this, but less experienced developers might not always make the right decision. That’s why some companies have a rule stating that each publicly accessible function or method in a header file should have a comment explaining what it does. Some organizations go even further and formalize these comments by explicitly listing the purpose of each method, what its arguments are, what values it returns, and possible exceptions it can throw.

A comment gives you the opportunity to state, in English, anything that you can’t state in code. For example, there’s really no way in C++ code to indicate that the saveRecord() method of a database object throws an exception if openDatabase() has not been called yet. A comment, however, can be the perfect place to note this restriction, as follows:

/*
 * This method throws a "DatabaseNotOpenedException"
 * if the openDatabase() method has not been called yet.
 */
int saveRecord(Record& record);

The C++ language forces you to specify the return type of a method, but it does not provide a way for you to say what the returned value actually represents. For example, the declaration of the saveRecord() method may indicate that it returns an int (a bad design decision discussed further in this section), but the client reading that declaration wouldn’t know what the int means. A comment explains the meaning of it:

/*
 * Returns: int
 *    An integer representing the ID of the saved record.
 * Throws:
 *    DatabaseNotOpenedException if the openDatabase() method has not
 *    been called yet.
 */
int saveRecord(Record& record);

As mentioned earlier, some companies require everything about a function to be documented in a formal way. The following is an example of how such a comment for the saveRecord() method might look like:

/*
 * saveRecord()
 * 
 * Saves the given record to the database.
 *
 * Parameters:
 *    Record& record: the record to save to the database.
 * Returns: int
 *    An integer representing the ID of the saved record.
 * Throws:
 *    DatabaseNotOpenedException if the openDatabase() method has not
 *    been called yet.
 */
int saveRecord(Record& record);

However, I don’t recommend this style of commenting. The first two lines are completely useless, since the name of the function is self-explanatory. The description of the parameter also does not add any additional information. Documenting what exactly the return type represents for this version of saveRecord() is required since it returns a generic int. However, a much better design would be to return a RecordID instead of a plain int, which removes the need to add any comments for the return type. RecordID could simply be a type alias (see Chapter 11) for int, but it conveys more information. The only comment that should remain is the exception. So, the following is my recommendation for the saveRecord() method:

/*
 * Throws:
 *    DatabaseNotOpenedException if the openDatabase() method has not
 *    been called yet.
 */
RecordID saveRecord(Record& record);

Sometimes, the parameters to, and the return type from, a function are generic and can be used to pass all kinds of information. In that case you need to clearly document exactly what type is being passed. For example, message handlers in Windows accept two parameters, LPARAM and WPARAM, and can return an LRESULT. All three can be used to pass anything you like, but you cannot change their type. By using type casting, they can, for example, be used to pass a simple integer or to pass a pointer to some object. Your documentation could look like this:

 * Parameters:
 *    WPARAM wParam: (WPARAM)(int): An integer representing...
 *    LPARAM lParam: (LPARAM)(string*): A string pointer representing...
 * Returns: (LRESULT)(Record*)
 *    nullptr in case of an error, otherwise a pointer to a Record object
 *    representing...

Commenting to Explain Complicated Code

Good comments are also important inside the actual source code. In a simple program that processes input from the user and writes a result to the console, it is probably easy to read through and understand all of the code. In the professional world, however, you will often need to write code that is algorithmically complex or too esoteric to understand simply by inspection.

Consider the code that follows. It is well written, but it may not be immediately apparent what it is doing. You might recognize the algorithm if you have seen it before, but a newcomer probably wouldn’t understand the way the code works.

void sort(int inArray[], size_t inSize)
{
    for (size_t i = 1; i < inSize; i++) {
        int element = inArray[i];
        size_t j = i - 1;
        while (j >= 0 && inArray[j] > element) {
            inArray[j+1] = inArray[j];
            j--;
        }
        inArray[j+1] = element;        
    }
}

A better approach would be to include comments that describe the algorithm that is being used, and to document (loop) invariants. Invariants are conditions that have to be true during the execution of a piece of code, for example, a loop iteration. In the modified function that follows, a thorough comment at the top explains the algorithm at a high level, and inline comments explain specific lines that may be confusing:

/*
 * Implements the "insertion sort" algorithm. The algorithm separates the 
 * array into two parts--the sorted part and the unsorted part. Each
 * element, starting at position 1, is examined. Everything earlier in the
 * array is in the sorted part, so the algorithm shifts each element over
 * until the correct position is found to insert the current element. When 
 * the algorithm finishes with the last element, the entire array is sorted.
 */
void sort(int inArray[], size_t inSize)
{
    // Start at position 1 and examine each element.
    for (size_t i = 1; i < inSize; i++) {
        // Loop invariant:
        //     All elements in the range 0 to i-1 (inclusive) are sorted.

        int element = inArray[i];
        // j marks the position in the sorted part after which element 
        // will be inserted.
        size_t j = i – 1;
        // As long as the current slot in the sorted array is higher than
        // element, shift values to the right to make room for inserting
        // (hence the name, "insertion sort") element in the right position.
        while (j >= 0 && inArray[j] > element) {
            inArray[j+1] = inArray[j];
            j--;
        }
        // At this point the current position in the sorted array
        // is *not* greater than the element, so this is its new position.
        inArray[j+1] = element;        
    }
}

The new code is certainly more verbose, but a reader unfamiliar with sorting algorithms would be much more likely to understand it with the comments included.

Commenting to Convey Meta-information

Another possible reason to use comments is to provide information at a higher level than the code itself. This meta-information provides details about the creation of the code without addressing the specifics of its behavior. For example, your organization may want to keep track of the original author of each method. You can also use meta-information to cite external documents or refer to other code.

The following example shows several instances of meta-information, including the author, the date it was created, and the specific feature it addresses. It also includes inline comments expressing metadata, such as the bug number that corresponds to a line of code and a reminder to revisit a possible problem in the code later.

/* 
 * Author:  marcg
 * Date:    110412
 * Feature: PRD version 3, Feature 5.10
 */
RecordID saveRecord(Record& record)
{
    if (!mDatabaseOpen) {
        throw DatabaseNotOpenedException();
    }
    RecordID id = getDB()->saveRecord(record);
    if (id == -1) return -1;  // Added to address bug #142 – jsmith 110428
    record.setId(id);
    // TODO: What if setId() throws an exception? – akshayr 110501
    return id;
}

A change-log could also be included at the beginning of each file. The following shows a possible example of such a change-log:

/*
 * Date     | Change
 *----------+--------------------------------------------------
 * 110413   | REQ #005: <marcg> Do not normalize values.
 * 110417   | REQ #006: <marcg> use nullptr instead of NULL.
 */

Another type of meta-information is a copyright notice. Some companies require such a copyright notice at the very beginning of every source file.

It’s easy to go overboard with comments. A good approach is to discuss which types of comments are most useful with your group and to form a policy. For example, if one member of the group uses a “TODO” comment to indicate code that still needs work, but nobody else knows about this convention, the code in need of attention could be overlooked.

Commenting Styles

Every organization has a different approach to commenting code. In some environments, a particular style is mandated to give the code a common standard for documentation. Other times, the quantity and style of commenting is left up to the programmer. The following examples depict several approaches to commenting code.

Commenting Every Line

One way to avoid lack of documentation is to force yourself to overdocument by including a comment for every line. Commenting every line of code should ensure that there’s a specific reason for everything you write. In reality, such heavy commenting on a large scale is unwieldy, messy, and tedious! For example, consider the following useless comments:

int result;                   // Declare an integer to hold the result.
result = doodad.getResult();  // Get the doodad's result.
if (result % 2 ==  0) {       // If the result modulo 2 is 0 ...
   logError();                // then log an error,
} else {                      // otherwise ...
   logSuccess();              // log success.
}                             // End if/else
return result;                // Return the result

The comments in this code express each line as part of an easily readable English story. This is entirely useless if you assume that the reader has at least basic C++ skills. These comments don’t add any additional information to code. Specifically, look at this line:

if (result % 2 == 0) {        // If the result modulo 2 is 0 ...

The comment is just an English translation of the code. It doesn’t say why the programmer has used the modulo operator on the result with the value 2. The following would be a better comment:

if (result % 2 == 0) {        // If the result is even ...

The modified comment, while still fairly obvious to most programmers, gives additional information about the code. The modulo operator with 2 is used because the code needs to check if the result is even.

Despite its tendency to be verbose and superfluous, heavy commenting can be useful in cases where the code would otherwise be difficult to comprehend. The following code also comments every line, but these comments are actually helpful:

// Calculate the doodad. The start, end, and offset values come from the
// table on page 96 of the "Doodad API v1.6".
result = doodad.calculate(kStart, kEnd, kOffset);
// To determine success or failure, we need to bitwise AND the result with
// the processor-specific mask (see "Doodad API v1.6", page 201).
result &= getProcessorMask();
// Set the user field value based on the "Marigold Formula."
// (see "Doodad API v1.6", page 136)
setUserField((result + kMarigoldOffset) / MarigoldConstant + MarigoldConstant);

This code is taken out of context, but the comments give you a good idea of what each line does. Without them, the calculations involving & and the mysterious “Marigold Formula” would be difficult to decipher.

Prefix Comments

Your group may decide to begin all source files with a standard comment. This is an excellent opportunity to document important information about the program and specific file. Examples of information that you might want to document at the top of every file include the following:

  • The last-modified date*
  • The original author*
  • A change-log (as described earlier)*
  • The feature ID addressed by the file
  • Copyright information
  • A brief description of the file/class
  • Incomplete features
  • Known bugs

The items marked with an asterisk are usually automatically handled by your source code control solution.

Your development environment may allow you to create a template that automatically starts new files with your prefix comment. Some source control systems such as Subversion (SVN) can even assist by filling in metadata. For example, if your comment contains the string $Id$, SVN can automatically expand the comment to include the author, filename, revision, and date.

An example of a prefix comment is shown here:

/*
 * $Id: Watermelon.cpp,123 2004/03/10 12:52:33 marcg $
 * 
 * Implements the basic functionality of a watermelon. All units are expressed
 * in terms of seeds per cubic centimeter. Watermelon theory is based on the
 * white paper "Algorithms for Watermelon Processing."
 *
 * The following code is (c) copyright 2017, FruitSoft, Inc. ALL RIGHTS RESERVED
 */

Fixed-Format Comments

Writing comments in a standard format that can be parsed by external document builders is an increasingly popular programming practice. In the Java language, programmers can write comments in a standard format that allows a tool called JavaDoc to automatically create hyperlinked documentation for the project. For C++, a free tool called Doxygen (available at www.doxygen.org) parses comments to automatically build HTML documentation, class diagrams, UNIX man pages, and other useful documents. Doxygen even recognizes and parses JavaDoc-style comments in C++ programs. The code that follows shows JavaDoc-style comments that are recognized by Doxygen:

/**
 * Implements the basic functionality of a watermelon
 * TODO: Implement updated algorithms!
 */
class Watermelon
{
    public:
        /**
         * @param initialSeeds The starting number of seeds, must be > 0.
         */
        Watermelon(int initialSeeds);

        /**
         * Computes the seed ratio, using the Marigold algorithm.
         * @param slowCalc Whether or not to use long (slow) calculations
         * @return The marigold ratio
         */
        double calcSeedRatio(bool slowCalc);
};

Doxygen recognizes the C++ syntax and special comment directives such as @param and @return to generate customizable output. An example of a Doxygen-generated HTML class reference is shown in Figure 3-1.

Illustration of Doxygen-generated HTML class reference.

FIGURE 3-1

Note that you should still avoid writing useless comments, including when you use a tool to automatically generate documentation. Take a look at the Watermelon constructor in the previous code. Its comment omits a description and only describes the parameter. Adding a description, as in the following example, is redundant:

        /**
         * The Watermelon constructor.
         * @param initialSeeds The starting number of seeds, must be > 0.
         */
        Watermelon(int initialSeeds);

Automatically generated documentation like the file shown in Figure 3-1 can be helpful during development because it allows developers to browse through a high-level description of classes and their relationships. Your group can easily customize a tool like Doxygen to work with the style of comments that you have adopted. Ideally, your group would set up a machine that builds documentation on a daily basis.

Ad Hoc Comments

Most of the time, you use comments on an as-needed basis. Here are some guidelines for comments that appear within the body of your code:

  • Avoid offensive or derogatory language. You never know who might look at your code someday.
  • Liberal use of inside jokes is generally considered okay. Check with your manager.
  • Before adding a comment, first consider whether you can rework the code to make the comment redundant. For example, by renaming variables, functions, and classes, by reordering steps in the code, by introducing intermediate well-named variables, and so on.
  • Imagine someone else is reading your code. If there are subtleties that are not immediately obvious, then you should document those.
  • Don’t put your initials in the code. Source code control solutions will track that kind of information automatically for you.
  • If you are doing something with an API that isn’t immediately obvious, include a reference to the documentation of that API where it is explained.
  • Remember to update your comments when you update the code. Nothing is more confusing than code that is fully documented with incorrect information.
  • If you use comments to separate a function into sections, consider whether the function might be broken into multiple, smaller functions.

Self-Documenting Code

Well-written code often doesn’t need abundant commenting. The best code is written to be readable. If you find yourself adding a comment for every line, consider whether the code could be rewritten to better match what you are saying in the comments. For example, use descriptive names for your functions, parameters, variables, classes, and so on. Properly make use of const; that is, if a variable is not supposed to be modified, mark it as const. Reorder the steps in a function to make it clearer what it is doing. Introduce intermediate well-named variables to make an algorithm easier to understand. Remember that C++ is a language. Its main purpose is to tell the computer what to do, but the semantics of the language can also be used to explain its meaning to a reader.

Another way of writing self-documenting code is to break up, or decompose, your code into smaller pieces. Decomposition is covered in detail in the following section.

DECOMPOSITION

Decomposition is the practice of breaking up code into smaller pieces. There is nothing more daunting in the world of coding than opening up a file of source code to find 300-line functions and massive, nested blocks of code. Ideally, each function or method should accomplish a single task. Any subtasks of significant complexity should be decomposed into separate functions or methods. For example, if somebody asks you what a method does and you answer, “First it does A, then it does B; then, if C, it does D; otherwise, it does E,” you should probably have separate helper methods for A, B, C, D, and E.

Decomposition is not an exact science. Some programmers will say that no function should be longer than a page of printed code. That may be a good rule of thumb, but you could certainly find a quarter-page of code that is desperately in need of decomposition. Another rule of thumb is that if you squint your eyes and look at the format of the code without reading the actual content, it shouldn’t appear too dense in any one area. For example, Figures 3-2 and 3-3 show code that has been purposely blurred so that you don’t focus on the content. It should be obvious that the code in Figure 3-3 has better decomposition than the code in Figure 3-2.

Illustration of computer code that is blurred.

FIGURE 3-2

Visual representation of computer code that is blurred.

FIGURE 3-3

Decomposition through Refactoring

Sometimes, when you’ve had a few coffees and you’re really in the programming zone, you start coding so fast that you end up with code that does exactly what it’s supposed to do, but is far from pretty. All programmers do this from time to time. Short periods of vigorous coding are sometimes the most productive times in the course of a project. Dense code also arises over the course of time as code is modified. As new requirements and bug fixes emerge, existing code is amended with small modifications. The computing term cruft refers to the gradual accumulation of small amounts of code that eventually turns a once-elegant piece of code into a mess of patches and special cases.

Refactoring is the act of restructuring your code. The following list contains example techniques that you can use to refactor your code. Consult one of the refactoring books in Appendix B to get a more comprehensive list.

  • Techniques that allow for more abstraction:
    • Encapsulate Field. Make a field private and give access to it with getter and setter methods.
    • Generalize Type. Create more general types to allow for more code sharing.
  • Techniques for breaking code apart into more logical pieces:
    • Extract Method. Turn part of a larger method into a new method to make it easier to understand.
    • Extract Class. Move part of the code from an existing class into a new class.
  • Techniques for improving names and the location of code:
    • Move Method or Move Field. Move to a more appropriate class or source file.
    • Rename Method or Rename Field. Change the name to better reveal its purpose.
    • Pull Up. In object-oriented programming, move to a base class.
    • Push Down. In object-oriented programming, move to a derived class.

Whether your code starts its life as a dense block of unreadable cruft or it just evolves that way, refactoring is necessary to periodically purge the code of accumulated hacks. Through refactoring, you revisit existing code and rewrite it to make it more readable and maintainable. Refactoring is an opportunity to revisit the decomposition of code. If the purpose of the code has changed or if it was never decomposed in the first place, when you refactor the code, squint at it and determine if it needs to be broken down into smaller parts.

When refactoring code, it is very important to be able to rely on a testing framework that catches any defects that you might introduce. Unit tests, discussed in Chapter 26, are particularly well suited for helping you catch mistakes during refactoring.

Decomposition by Design

If you use modular decomposition and approach every module, method, or function by considering what pieces of it you can put off until later, your programs will generally be less dense and more organized than if you implement every feature in its entirety as you code.

Of course, you should still design your program before jumping into the code.

Decomposition in This Book

You will see decomposition in many of the examples in this book. In many cases, methods are referred to for which no implementation is shown because they are not relevant to the example and would take up too much space.

NAMING

The C++ compiler has a few naming rules:

  • Names cannot start with a number (for example, 9to5).
  • Names that contain a double underscore (such as my__name) are reserved and shall not be used.
  • Names that begin with an underscore (such as _name or _Name) are reserved and shall not be used.

Other than those rules, names exist only to help you and your fellow programmers work with the individual elements of your program. Given this purpose, it is surprising how often programmers use unspecific or inappropriate names.

Choosing a Good Name

The best name for a variable, method, function, parameter, class, namespace, and so on accurately describes the purpose of the item. Names can also imply additional information, such as the type or specific usage. Of course, the real test is whether other programmers understand what you are trying to convey with a particular name.

There are no set-in-stone rules for naming other than the rules that work for your organization. However, there are some names that are rarely appropriate. The following table shows some names at both ends of the naming continuum.

GOOD NAMES BAD NAMES
sourceName, destinationName
Distinguishes two objects
thing1, thing2
Too general
gSettings
Conveys global status
globalUserSpecificSettingsAndPreferences
Too long
mNameCounter
Conveys data member status
mNC
Too obscure, too brief
calculateMarigoldOffset()
Simple, accurate
doAction()
Too general, imprecise
mTypeString
Easy on the eyes
typeSTR256
A name only a computer could love
mIHateLarry
Unacceptable inside joke
errorMessage
Descriptive name
string
Non-descriptive name
sourceFile, destinationFile
No abbreviations
srcFile, dstFile
Abbreviations

Naming Conventions

Selecting a name doesn’t always require a lot of thought and creativity. In many cases, you’ll want to use standard techniques for naming. Following are some of the types of data for which you can make use of standard names.

Counters

Early in your programming career, you probably saw code that used the variable “i” as a counter. It is customary to use i and j as counters and inner-loop counters, respectively. Be careful with nested loops, however. It’s a common mistake to refer to the “ith” element when you really mean the “jth” element. When working with 2-D data, it’s probably easier to use row and column as indices, instead of i and j. Some programmers prefer using counters outerLoopIndex and innerLoopIndex, and some even frown upon using i and j as loop counters.

Prefixes

Many programmers begin their variable names with a letter that provides some information about the variable’s type or usage. On the other hand, there are as many programmers, or even more, who disapprove of using any kind of prefix because this could make evolving code less maintainable in the future. For example, if a member variable is changed from static to non-static, you have to rename all the uses of that name. If you don’t rename them, your names continue to convey semantics, but now they are the wrong semantics.

However, you often don’t have a choice and you need to follow the guidelines of your company. The following table shows some potential prefixes.

PREFIX EXAMPLE NAME LITERAL PREFIX MEANING USAGE
m
m_
mData
m_data
“member” Data member within a class
s
ms
ms_
sLookupTable
msLookupTable
ms_lookupTable
“static” Static variable or data member
k kMaximumLength “konstant” (German for “constant”) A constant value. Some programmers use all uppercase names without a prefix to indicate constants.
b
is
bCompleted
isCompleted
“Boolean” Designates a Boolean value
n
mNum
nLines
mNumLines
“number” A data member that is also a counter. Because an “n” looks similar to an “m,” some programmers instead use mNum as a prefix.

Hungarian Notation

Hungarian Notation is a variable and data-member–naming convention that is popular with Microsoft Windows programmers. The basic idea is that instead of using single-letter prefixes such as m, you should use more verbose prefixes to indicate additional information. The following line of code shows the use of Hungarian Notation:

char* pszName; // psz means "pointer to a null-terminated string"

The term Hungarian Notation arose from the fact that its inventor, Charles Simonyi, is Hungarian. Some also say that it accurately reflects the fact that programs using Hungarian Notation end up looking as if they were written in a foreign language. For this latter reason, some programmers tend to dislike Hungarian Notation. In this book, prefixes are used, but not Hungarian Notation. Adequately named variables don’t need much additional context information besides the prefix. For example, a data member named mName says it all.

Getters and Setters

If your class contains a data member, such as mStatus, it is customary to provide access to the member via a getter called getStatus() and a setter called setStatus(). To give access to a Boolean data member, you typically use is as a prefix instead of get, for example isRunning(). The C++ language has no prescribed naming for these methods, but your organization will probably want to adopt this or a similar naming scheme.

Capitalization

There are many different ways of capitalizing names in your code. As with most elements of coding style, it is very important that your group adopts a standardized approach and that all members adopt that approach. One way to get messy code is to have some programmers naming classes in all lowercase with underscores representing spaces (priority_queue) and others using capitals with each subsequent word capitalized (PriorityQueue). Variables and data members almost always start with a lowercase letter and use either underscores (my_queue) or capitals (myQueue) to indicate word breaks. Functions and methods are traditionally capitalized in C++, but, as you’ve seen, in this book I have adopted the style of lowercase functions and methods to distinguish them from class names. A similar style of capitalizing letters is used to indicate word boundaries for class and data member names.

Namespaced Constants

Imagine that you are writing a program with a graphical user interface. The program has several menus, including File, Edit, and Help. To represent the ID of each menu, you may decide to use a constant. A perfectly reasonable name for a constant referring to the Help menu ID is kHelp.

The name kHelp will work fine until you add a Help button to the main window. You also need a constant to refer to the ID of the button, but kHelp is already taken.

A possible solution for this is to put your constants in different namespaces, which are discussed in Chapter 1. You create two namespaces: Menu and Button. Each namespace has a kHelp constant and you use them as Menu::kHelp and Button::kHelp. Another, and more recommended solution is to use enumerated types, also introduced in Chapter 1.

USING LANGUAGE FEATURES WITH STYLE

The C++ language lets you do all sorts of terribly unreadable things. Take a look at this wacky code:

i++ + ++i;

This is unreadable but more importantly, its behavior is undefined by the C++ standard. The problem is that i++ uses the value of i but has a side effect of incrementing it. The standard does not say when this incrementing should be done, only that the side effect (increment) should be visible after the sequence point “;”, but the compiler can do it at any time during the execution of that line. It’s impossible to know which value of i will be used for the ++i part. Running this code with different compilers and platforms can result in different values.

The following is another example of code which you should avoid, because it exhibits undefined behavior if your compiler is not C++17 compliant. With C++17, this has well-defined behavior: first i is incremented, and then used as index in a[i].

a[i] = ++i;

With all the power that the C++ language offers, it is important to consider how the language features can be used toward stylistic good instead of evil.

Use Constants

Bad code is often littered with “magic numbers.” In some function, the code might be using 2.71828. Why 2.71828? What does that value mean? People with a mathematical background might find it obvious that this represents an approximation of the transcendental value e, but most people don’t know this. The language offers constants to give a symbolic name to a value that doesn’t change, such as 2.71828.

const double kApproximationForE = 2.71828182845904523536;

Use References Instead of Pointers

Traditionally, C++ programmers learned C first. In C, pointers were the only pass-by-reference mechanism, and they certainly worked just fine for many years. Pointers are still required in some cases, but in many situations you can switch to references. If you learned C first, you probably think that references don’t really add any new functionality to the language. You might think that they merely introduce a new syntax for functionality that pointers could already provide.

There are several advantages to using references rather than pointers. First, references are safer than pointers because they don’t deal directly with memory addresses and cannot be nullptr. Second, references are more stylistically pleasing than pointers because they use the same syntax as stack variables, avoiding symbols such as * and &. They’re also easy to use, so you should have no problem adopting references into your style palette. Unfortunately, some programmers think that if they see an & in a function call, they know the called function is going to change the object, and if they don’t see the & it must be pass-by-value. With references, they say they don’t know if the function is going to change the object unless they look at the function prototype. This is a wrong way of thinking. Passing in a pointer does not automatically mean that the object will be modified, because the parameter might be const T*. Passing both a pointer and a reference can modify the object, or it may not, depending on whether the function prototype uses const T*, T*, const T&, or T&. So, you need to look at the prototype anyway to know if the function might change the object.

Another benefit of references is that they clarify ownership of memory. If you are writing a method and another programmer passes you a reference to an object, it is clear that you can read and possibly modify the object, but you have no easy way of freeing its memory. If you are passed a pointer, this is less clear. Do you need to delete the object to clean up memory? Or will the caller do that? Note that the preferred way of handling memory is to use smart pointers, introduced in Chapter 1.

Use Custom Exceptions

C++ makes it easy to ignore exceptions. Nothing about the language syntax forces you to deal with exceptions and you could easily write error-tolerant programs with traditional mechanisms such as returning nullptr or setting an error flag.

Exceptions provide a much richer mechanism for error handling, and custom exceptions allow you to tailor this mechanism to your needs. For example, a custom exception type for a web browser could include fields that specify the web page that contained the error, the network state when the error occurred, and additional context information.

Chapter 14 contains a wealth of information about exceptions in C++.

FORMATTING

Many programming groups have been torn apart and friendships ruined over code-formatting arguments. In college, a friend of mine got into such a heated debate with a peer over the use of spaces in an if statement that people were stopping by to make sure that everything was okay.

If your organization has standards in place for code formatting, consider yourself lucky. You may not like the standards they have in place, but at least you won’t have to argue about them.

If no standards are in place for code formatting, I recommend to introduce them in your organization. Standardized coding guidelines make sure that all programmers on your team follow the same naming conventions, formatting rules, and so on, which makes the code more uniform and easier to understand.

If everybody on your team is just writing code their own way, try to be as tolerant as you can. As you’ll see, some practices are just a matter of taste, while others actually make it difficult to work in teams.

The Curly Brace Alignment Debate

Perhaps the most frequently debated point is where to put the curly braces that demark a block of code. There are several styles of curly brace use. In this book, the curly brace is put on the same line as the leading statement, except in the case of a function, class, or method name. This style is shown in the code that follows (and throughout this book):

void someFunction()
{
    if (condition()) {
        cout << "condition was true" << endl;
    } else {
        cout << "condition was false" << endl;
    }
}

This style conserves vertical space while still showing blocks of code by their indentation. Some programmers would argue that preservation of vertical space isn’t relevant in real-world coding. A more verbose style is shown here:

void someFunction()
{
    if (condition())
    {
        cout << "condition was true" << endl;
    }
    else
    {
        cout << "condition was false" << endl;
    }
}

Some programmers are even liberal with the use of horizontal space, yielding code like this:

void someFunction()
{
    if (condition()) 
        {
            cout << "condition was true" << endl;
        }
    else 
        {
            cout << "condition was false" << endl;
        }
} 

Another point of debate is whether or not to put braces around single statements, for example:

void someFunction()
{
    if (condition())
        cout << "condition was true" << endl;
    else
        cout << "condition was false" << endl;
}

Of course, I won’t recommend any particular style because I don’t want hate mail.

Coming to Blows over Spaces and Parentheses

The formatting of individual lines of code can also be a source of disagreement. Again, I won’t advocate a particular approach, but you are likely to encounter a few of the styles shown here.

In this book, I use a space after any keyword, a space before and after any operator, a space after every comma in a parameter list or a call, and parentheses to clarify the order of operations, as follows:

if (i == 2) {
    j = i + (k / m);
}

An alternative, shown next, treats if stylistically like a function, with no space between the keyword and the left parenthesis. Also, the parentheses used to clarify the order of operations inside of the if statement are omitted because they have no semantic relevance.

if( i == 2 ) {
    j = i + k / m;
}

The difference is subtle, and the determination of which is better is left to the reader, yet I can’t move on from the issue without pointing out that if is not a function.

Spaces and Tabs

The use of spaces and tabs is not merely a stylistic preference. If your group does not agree on a convention for spaces and tabs, there are going to be major problems when programmers work jointly. The most obvious problem occurs when Alice uses four-space tabs to indent code and Bob uses five-space tabs; neither will be able to display code properly when working on the same file. An even worse problem arises when Bob reformats the code to use tabs at the same time that Alice edits the same code; many source code control systems won’t be able to merge in Alice’s changes.

Most, but not all, editors have configurable settings for spaces and tabs. Some environments even adapt to the formatting of the code as it is read in, or always save using spaces even if the Tab key is used for authoring. If you have a flexible environment, you have a better chance of being able to work with other people’s code. Just remember that tabs and spaces are different because a tab can be any length and a space is always a space.

STYLISTIC CHALLENGES

Many programmers begin a new project by pledging that this time, they will do everything right. Any time a variable or parameter shouldn’t be changed, it’ll be marked const. All variables will have clear, concise, readable names. Every developer will put the left curly brace on the subsequent line and will adopt the standard text editor and its conventions for tabs and spaces.

For a number of reasons, it is difficult to sustain this level of stylistic consistency. In the case of const, sometimes programmers just aren’t educated about how to use it. You will eventually come across old code or a library function that isn’t const-savvy. A good programmer will use const_cast() to temporarily suspend the const property of a variable, but an inexperienced programmer will start to unwind the const property back from the calling function, once again ending up with a program that never uses const.

Other times, standardization of style comes up against programmers’ individual tastes and biases. Perhaps the culture of your team makes it impractical to enforce strict style guidelines. In such situations, you may have to decide which elements you really need to standardize (such as variable names and tabs) and which ones are safe to leave up to individuals (perhaps spacing and commenting style). You can even obtain or write scripts that will automatically correct style “bugs” or flag stylistic problems along with code errors. Some development environments, such as Microsoft Visual C++ 2017, support automatic formatting of code according to rules that you specify. This makes it trivial to write code that always follows the guidelines that have been configured.

SUMMARY

The C++ language provides a number of stylistic tools without any formal guidelines on how to use them. Ultimately, any style convention is measured by how widely it is adopted and how much it benefits the readability of the code. When coding as part of a team, you should raise issues of style early in the process as part of the discussion of what language and tools to use.

The most important point about style is to appreciate that it is an important aspect of programming. Teach yourself to check over the style of your code before you make it available to others. Recognize good style in the code you interact with and adopt the conventions that you and your organization find useful.

This chapter concludes the first part of this book. The next part discusses software design on a high level.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset