CS 70

Coding Idioms

One goal of CS70 is to teach professional coding practices. Nearly all languages have idioms (agreed upon best practices), and good code will usually adhere to the idioms of the language in which it was written. By nature, idioms are subjective and often change over time.

In CS70, we will hold you accountable for abiding by the following set of C++ idioms. You may disagree with some of them, but we ask that you follow them for the code that you write in this class. (This will be good practice, since in industry, you will likely find yourself required to follow coding practices that you may not agree with). Each numbered item below corresponds to a potential point deduction in Gradescope, so it is in your best interest to treat this page as a checklist to review before submitting each assignment.

This page contains all of the idioms that you will need throughout the entire semester. For the early homeworks, not all of these will be relevant (for example, you do not write any classes or iterators in hw2). The following list highlights the relevant idioms for each homework, and will be updated every week.

  • HW2: Section 1 (General) and Section 2 (Conciseness).
  • HW3: Section 1 (General), Section 2 (Conciseness), and Section 4 (Classes), although 4.4 is not relevant for this assignment. Note that the coordinate values in this assignment can sometimes be negative, so you should not use size_t for them, even though they're sometimes used as array indices. (So, in other words, 1.4 is not relevant for this assignment.)
  • HW4: Sections 1–4, although 4.4 is not relevant for this assignment.
  • HW5: All sections are relevant for this assignment. Pay close attention to Sections 4.4 and 5, as this is the first assignment in which these idioms are relevant.
  • HW6 and onward: All sections are relevant for these assignments.

Contents

1. General

  1. Prefer pre-increment over post-increment
  2. Choose the right loop for the job (for vs. while)
  3. Avoid magic numbers
  4. Use size_t for machine-relevant sizes
  5. Use include guards

2. Conciseness

  1. Avoid excessive unnecessary variables
  2. Avoid unnecessary conditionals
  3. Avoid duplicate code between cases
  4. Avoid unreachable code
  5. Avoid unused variables
  6. Avoid comparing bools to true or false

3. Pointers

  1. Prefer [] notation
  2. Prefer -> notation

4. Classes

  1. Prefer member initialization list
  2. Avoid unnecessary this keywords
  3. Label methods const when applicable
  4. Implement != with ==
  5. Prefer synthesized methods when applicable
  6. Avoid explicitly calling operators and destructors

5. Iterators

  1. Use standard template aliases
  2. Implement operator-> with operator*

1. General

1.1 Prefer pre-increment over post-increment

In C++, we can either pre-increment a variable by placing ++ in front (such as ++x) or post-increment a variable by placing ++ after (such as x++). Both increase the variable by 1, but pre-increment returns the incremented variable and post-increment returns the value before the variable was incremented.

Consider the following example:

int x = 5;
++x; // pre-increment x
x++; // post-increment x

x = 42;
std::cout << ++x << std::endl; // this will set x to 43 and print "43"
x = 42;
std::cout << x++ << std::endl; // this will set x to 43 but print "42"

Most of the time (such as in for loops), we only care about incrementing the variable and do not care about the value returned. When both pre-increment and post-increment have the desired effect, we should use pre-increment because it is more efficient (it takes 1 hardware instruction rather than 2). We also prefer pre-increment to += 1.

1.2 Choose the Right Loop for the Job (for vs. while)

Although for and while loops perform similar functions (and, in fact, any for loop can be rewritten as a while loop and vice versa), the two types of loops carry different implications, so choosing the correct loop form increases the readability of our code and can help clarify our intent.

When to Use a for Loop

It makes most sense to use a for loop when we have a well-defined, finite number of iterations, because

  • We are counting through a specific range of values; or,
  • We are iterating through the elements of a data structure with a known size.

Just reading the start of a for loop (the part inside the parentheses) tells someone a lot about what the loop is doing. For example, consider this loop:

for (size_t i = 0; i < 5; ++i) {
    std::cout << i << std::endl;
    // Some other code, not shown...
}

When you look at this code, you'll probably think, “Oh, this code is going to print the numbers 0 through 4.” You can tell that it's going to do that because the loop starts with i being set to 0, and then it's going stop when i is 5 (without executing the body at that point); each time through the loop it prints i and then increments i. An experienced programmer sees the first line and immediately knows what the loop is going to do.

Thus it would be very misleading to write this code, which looks initially like the same kind of loop, but isn't:

for (size_t i = 0; i < 5; ++i) {
    std::cout << i << std::endl;
    i += (i % 2)
    if ((i + 10) % 7 == 6) {
        break;
    }
}

The above code only prints the numbers 0, 1, and 3. It's confusing to read because the for loop code at the top signals, “Oh, this is just a simple loop that's going to go through the numbers 0 to 4,” but then the code does something else. This code isn’t buggy, but it's confusing because it doesn't do what the for loop notation at the top suggests it's going to do. (Congratulations if you figured out that it would stop when i is 3 because the 3 + 10 is 13, and 13 divided by 7 is 1 with a remainder of 6!)

Conversely, this code is also confusing:

size_t i = 0;
while (i < 5) {
    std::cout << i << std::endl;
    ++i;
}  // Assume i is never used again outside of the loop

This while loop prints out the numbers 0 through 4, but you have to read the code carefully to make sure it's not doing anything else clever. It could be expressed more readably as a simple for loop. Someone reading it may be puzzled, they may think “there must be some reason this programmer used a while loop here instead of a for loop, but I don't see it.” And if there isn't a reason for using a while loop, using one is just confusing.

So, use a for loop when you need to count through a range of values, or when you're iterating through a data structure with a known size and you want people to easily see what you're doing.

And, if you do have anything clever going on in your for loop, you need to make it clear what you're doing at the outset. For example, if you're going to break out of the loop early, you should say so in front of your for statement:

int find42(int* elements, size_t count) {
    // bails out of the loop early if it finds 42
    for (size_t i = 0; i < count; ++i) {
        if (elements[i] == 42) {
            return i;
        }
    }
    return -1;
}

(In fact, some famous computer scientists consider this sort of loop “sinful”, because they would claim that every for loop should execute for a specific known number of times as specified in the for statement. In this class, we consider for loops that exit early acceptable, so long as it’s abundantly clear what they’re doing.)

Looping Over Data Structures

We can also use a for loop with an iterator to walk through the elements of a data structure. For example, the following function prints every element in a std::list<int> (a list of ints):

void printList(const std::list<int>& l) {
    for (std::list<int>::const_iterator i = l.begin(); i != l.end(); ++i) {
        std::cout << *i << std::endl;
    }
}

Here, we're declaring an iterator i that starts at the beginning of the list, and then we're going to keep going until are “past the end” of the list (end() is not the last element of the list, it's where we end up after the last element of the list). The increment operation ++i moves the iterator to the next element of the list.

The type of the iterator is a little unwieldy, which we can fix using C++'s auto keyword which infers the type of a variable from its initializer:

void printList(const std::list<int>& l) {
    for (auto i = l.begin(); i != l.end(); ++i) {
        std::cout << *i << std::endl;
    }
}
More Offbeat for Loops

In fact, we can even declare and use multiple iterators in a single for loop.

void printTwoVectors(const std::list<int>& l1, const std::list<int>& l2) {
    for (std::vector::const_iterator i = l1.begin(), j = l2.begin();
        i != l1.end() && j != l2.end(); ++i, ++j) {
        std::cout << *i << ", " << *j << std::endl;
    }
}

To an experienced programmer, this second loop is still fairly clear—we can know just from looking at the for loop statement that it's going to iterate through both lists at the same time and stop when either list runs out. But it's still a fairly unfamiliar pattern to most programmers, and so probably deserves a comment.

Relatedly, while it’s fairly straightforward to count backwards (producing the values 9 through 0) when we have an int variable, with a loop like

for (int i = 9; i != -1; --i) {
    std::cout << i << std::endl;
}

…we can’t do the same style of for loop when it’s a size_t, because i can’t ever be negative. There is a way some people write a backwards-counting for loop for a size_t, but although quite widely used in real-world code, it is very confusing to read to those unfamiliar with this pattern:

for (size_t i = 10; i --> 0;) {
    std::cout << i << std::endl;
}

This loop also prints out the numbers 9 through 0. But it's rather too cute and “clever”. It looks like there is a magical --> operator, that means “goes to zero”. But there isn't any such operator. Instead, i --> 0 is parsed as (i--) > 0, namely it is the post-decrement operator followed by the greater-than operator. So, this loop is equivalent to:

size_t i = 10;
while (i > 0) {
    --i;
    std::cout << i << std::endl;
}

which is probably easier for most people to understand than grasping the trickiness of the post-decrement operator and placing the decrement in the condition rather than the usual place.

When to Use a while Loop

A while loop is best used when

  • There is no need for a counter or loop variable; or,
  • The loop logic is more complex and so using a for loop would be misleading.

For example, the following code prints and removes all the elements of a std::stack.

// Approach 1 (a): for loop (not preferred)
void dumpStack(std::stack& s) {
    for (size_t i = 0; i < s.size(); ++i) {
        std::cout << s.top() << std::endl;
        s.pop();
    }
}

There are two issues with this code. First, it actually has a bug. As we empty the stack, s.size() decreases, so we will only pop half of the elements. We could fix this bug by writing

// Approach 1 (b): bug-fixed for loop (not preferred)
void dumpStack(std::stack& s) {
    for (size_t i = 0, origSize = s.size(); i < origSize; ++i) {
        std::cout << s.top() << std::endl;
        s.pop();
    }
}

But this version is still not ideal. It's now no longer a familiar-looking for loop—it's got extra stuff in it for us to puzzle over. And we still have the second issue in the original code—the i variable is completely unnecessary, as it's never used except in the for statement.

It turns out there is a much easier way to write this loop:

// Approach 2: while loop (preferred)
void dumpStack(std::stack& s) {
    while (!s.empty()) {
        std::cout << s.top() << std::endl;
        s.pop();
    }
}

This loop is much easier to read: “Keep popping elements until the stack is empty”. For some implementations of std::stack, this version is also more efficient, because s.size() may take longer to compute than s.empty(), especially compared to the first version of the code where we called s.size() on every iteration of the loop.

1.3 Avoid magic numbers

If your code requires a specific literal value (such as a particular number or string), you should usually store the value as a constant rather than using the literal directly in your code. Not only does this make it easier to change the value in the future, it allows you to give the constant a meaningful name which helps provide the reader with context.

In general, we declare a constant in the .hpp file of a class, but if it is only need in a particular function, it may make sense to declare the constant at the beginning of the function body.

1.4 Use size_t for machine-relevant sizes

Whenever we have a count of “how much data we're storing” (e.g., the size of an array, or the number of items in a list), we represent that number using the C++ size_t data type, which is an unsigned type whose range varies based on the kind of machine we're running on (i.e., it has a bigger range on 64-bit machines than 32-bit machines).

When you write a loop index variable that also represents this kind of count (e.g., counting through the elements of an array), that should also be a size_t.

Do not use a size_t in other situations unrelated to machine-relevant sizes (e.g., you have asked someone what day of the month they were born on, and you know that days of the month will never be negative).

Also, because size_t is an unsigned type, if you ever try to set a size_t variable to a negative number, it will wrap-around following the rules of modular arithmetic and give you a very large positive number.

size_t x = 0;
--x; // x is now the largest possible positive size_t value

1.5 Use include guards

On its own, #include has the danger of including the same code multiple times. To prevent this, we should place an include guard at the top of every .hpp file (except for -private.hpp files when templating). An include guard makes sure that a file is only included once by defining a unique variable associated with each file. For example, if we have a file sheep.hpp, we should structure the file as follows.

##ifndef SHEEP_HPP_
##define SHEEP_HPP_

// (Contents of sheep.hpp)

##endif  // SHEEP_HPP_

This will define the preprocessor macro SHEEP_HPP_ the first time that we include sheep.hpp, and then use the existence of that variable to avoid including sheep.hpp a second time if it is included again. For this to work, must use a unique preprocessor macro name for each .hpp file, so it is idiomatic to use the path and name of the file.

You can see a more detailed explanation of include guards in chapter 1.3 of the CS70 Textbook.

2. Conciseness

2.1 Avoid excessive unnecessary variables

In C++, we strive for clear and concise code and avoid unnecessary “bloat” which can distract the reader. Consider the following two approaches for calculating Euclidean distance:

// Approach 1: excessive unnecessary variables (not preferred)
float euclideanDistance(float x, float y) {
    float xSquared = x * x;
    float ySquared = y * y;
    float distance = sqrt(xSquared + ySquared);
    return distance;
}

// Approach 2: concise implementation (preferred)
float euclideanDistance(float x, float y) {
    return sqrt(x * x + y * y);
}

The xSquared and ySquared variables in the first approach aren't adding anything useful to the code—it actually takes more space to say xSquared than it does to say x * y and it tells us nothing more.

In some cases, however, an unnecessary temporary variable can add clarity by distilling out an intermediate result and giving it a meaningful name.

Overall, you need to consider whether your variables make the code easier or harder to follow. We recognize that sometimes reasonable people can disagree about what is most readable, but likewise sometimes there is code where almost everyone agrees that added variables aren't helping. It's only in those latter cases that we'd take off points for this issue.

2.2 Avoid unnecessary conditionals

Unnecessary conditional statements hinder code readability and can significantly reduce performance due to branch prediction, which you will learn more about in CS 105. Thus, you should usually use conditional statements only when they are required for the correct execution of your code.

Below, we discuss a number of common pitfalls involving unnecessary conditionals.

Returning booleans

When a function returns whether a condition is true, it should return the condition directly rather than using an if-else statement to return true or false. Consider the following two implementations of a the isPositivefunction, which checks whether an integer is positive.

// Approach 1: if-else (not preferred)
isPositive(int x) {
    if (x > 0) {
        return true;
    } else {
        return false;
    }
}

// Approach 2: return condition (preferred)
isPositive(int x) {
    return x > 0;
}

Returning inside of an if

If we always return inside of an if block, we should not include else for cases not handled by the if. When the if condition is met, we will return before any of the other code is run, so the else is unnecessary.

For example, suppose that the Sheep class has a getAge() method which returns 0 if ageUnknown_ is true, and otherwise returns age_.

// Approach 1: Including the else (not preferred)
size_t Sheep::getAge() {
    if (ageUnknown_) {
        return 0;
    } else {
        return age_;
    }
}

// Approach 2: Not including the else (preferred)
size_t Sheep::getAge() {
    if (ageUnknown_) {
        return 0;
    }
    return age_;
}

Factor conditionals out of loops

When you have a conditional statement inside the body of a loop, consider whether it can be moved outside of the loop. If so, this will cause it to only run once rather than every iteration of the loop.

For example, suppose that we want to print the numbers 0 through 4 if and only if the verbose flag is set to true. Rather than checking each iteration of the loop, we should only enter the loop if verbose == true.

// Approach 1: check condition in loop (not preferred)
for (size_t i = 0; i < 4; ++i) {
    if (verbose) {
        std::cout << i << std::endl;
    }
}

// Approach 2: factor condition out of loop (preferred)
if (verbose) {
    for (size_t i = 0; i < 4; ++i) {
        std::cout << i << std::endl;
    }
}

Conditionals already handled by loop conditions

Frequently, we want a loop to only run if a certain condition is true. Remember that loops already contain a looping condition, so you only need to add an additional if statement if the condition cannot be incorporated into the looping condition.

In the following example, the if statement can be removed because it is captured by the looping condition. If input <= 0, the loop will not be run because i < input will never be true.

if (input > 0) {
    for (size_t i = 0; i < input; ++i) {
        std::cout << i << std::endl;
    }
}

In the following example, the if condition is separate from the looping condition, so it does not need to be removed. Assume that verbose is a bool which is set somewhere above.

if (verbose) {
    for (size_t i = 0; i < input; ++i) {
        std::cout << i << std::endl;
    }
}

2.3 Avoid duplicate code between cases

Often times, a function will consist of multiple cases. Some work will be unique to each case, while other work must be done for all cases. We should always factor out shared work so that the code doing this work is only written once.

For example, suppose that the Sheep class has a birthday method which celebrates the sheep’s birthday. The method will always increment the sheep’s age and takes the parameter likesCake indicating whether they should eat cake or ice cream.

// Approach 1: Duplicate code between cases (not preferred)
void Sheep::birthday(bool likesCake) {
    if (likesCake) {
        eatCake();
        ++age_;
    } else {
        eatIceCream();
        ++age_;
    }
}

// Approach 2: Shared work factored out (preferred)
void Sheep::birthday(bool likesCake) {
    if (likesCake) {
        eatCake();
    } else {
        eatIceCream();
    }
    ++age_;
}

Shared work does not have to be an exact copy across the cases; sometimes, the shared work looks different in each case but can be rewritten in a more general way that can be executed outside of the cases.

2.4 Avoid unreachable code

Unreachable code is code which can never be run. We should always remove unreachable code since it can confuse the reader and does not affect the behavior of our program.

Here are two examples of unreachable code.

// Example 1: Code after a return
int foo(int* x, int* y) {
    int temp = *x;
    *x = *y;
    return *x + *y;
    *y = temp;  // This line is unreachable and will never be run
}

// Example 2: Code in a conditional that will never be executed
int x;
std::cin >> x;
if (x > 0 && x < 0) {  // This condition will never evaluate to true
    x = 42;            // This line is unreachable and will never be run
}

2.5 Avoid unused variables

An unused variable is a variable which is never read from and therefore does not affect the behavior of our program. We should always remove unused variables because they can confuse the reader and distract from the purpose of our program. Even if you write to a variable, it is still unused if you never read from it. Depending on the settings we use, the compiler will often identify unused variables with a warning.

In the following example, x and y are unused variables and should be removed

int main() {
    int x = 5;  // x is an unused variable

    int y = 0;  // y is also unused even though we write to it
    y += 5;

    int z = 8;  // because we read from z, it is not an unused variable
    std::cout << z << std::endl;

    return 0;
}

2.6 Avoid comparing bools to true or false

Comparing a bool to true or false adds unnecessary obfuscation, making our code more complicated and harder to understand. For example, the expression a == true can always be replaced with a, since a must already be a bool to be compared to true. Similarly, a == false can always be replaced with !a. Our code should never have the phrase == true, != true, == false, or != false.

The following example demonstrates how we can rewrite the conditions of a function to remove unnecessary comparisons.

// Approach 1: Compares bools to true and false (not preferred)
void foo(bool a, bool* b) {
    if (a == true) {
        std::cout << "case 1" << std::endl;
    } else {
        std::cout << "case 2" << std::endl;
        if (b[0] == false) {
            std::cout << "subcase 1" << std::endl;
        } else {
            std::cout << "subcase 2" << std::endl;
        }
    }
}

// Approach 2: Removes unnecessary comparisons (preferred)
void foo(bool a, bool* b) {
    if (a) {
        std::cout << "case 1" << std::endl;
    } else {
        std::cout << "case 2" << std::endl;
        if (!b[0]) {
            std::cout << "subcase 1" << std::endl;
        } else {
            std::cout << "subcase 2" << std::endl;
        }
    }
}

3. Pointers

3.1 Prefer [] notation

If p is a pointer and n is an integral datatype (such as int or size_t), then p[n] is syntactic sugar for *(p + n). In C++, all arrays are pointers, so this is especially helpful for accessing the nth element of an array. We should use [] notation instead of pointer math whenever applicable.

string sheep[] = {"Shawn", "Elliot", "Timmy", "Shirley"};

// Approach 1: Pointer math (not preferred)
std::cout << *(sheep + 2) << std::endl;

// Approach 2: [] notation (preferred)
std::cout << sheep[2] << std::endl;

3.2 Prefer -> notation

Suppose that p is a pointer to an object with public member function foo() and public member variable x. Then, p->foo() is syntactic sugar for (*p).foo() and p->x is syntactic sugar for (*p).x. We should use arrow notation whenever applicable.

Sheep shawn = new Sheep();

// Approach 1: Explicit dereference (not preferred)
(*shawn).pet();

// Approach 2: Arrow notation (preferred)
shawn->pet();

4. Classes

4.1 Prefer member initialization list

When we initialize an object in C++, all of its data member need to be initialized as well. We specify the value to which each variable is initialized in the member initialization list of our object’s constructor. Any data member not in the member initialization list is initialized as specified in the class definition, and if we did not give a specific initialization there, it is default initialized.

Any time we refer to a member variable in the body of the constructor, the variable is in its use phase. Therefore, if we set the variable in the body of the constructor, we really are setting it twice: first, we default initialize it to the wrong value, then update it to the correct value. When we use the member initialization list, we set the variable to the correct value the first time. For this reason, we should always use the member initialization list to initialize member variables rather than setting them in the body of the constructor.

The member initialization list has the form : dataMember_{parameters}, ..., where ... can be a list of more data members with the same format. The dataMember_{parameters} term calls the constructor of dataMember_ and passes it parameters—often it's a call to the copy constructor, but it doesn't have to be, it can be any constructor for that type. Let’s consider a simple parameterized constructor for a Point class.

// Approach 1: Set variables in constructor body (not preferred)
Point::Point(double x, double y) {  // x_ and y_ are default initialized
    // x_ and y_ are updated to x and y in the use phase
    x_ = x;
    y_ = y;
}

// Approach 2: Initialize variables in member initialization list (preferred)
Point::Point(double x, double y)
    :  // x_ and y_ are initialized to x and y
      x_{x},
      y_{y} {
    // Nothing to do in constructor body
}

Next, let’s consider a more complex example. Suppose that Sheep has a parameterized constructor taking a string name and size_t age. Suppose that the Barn class has a Sheep sheep_ data member and a Sheep* sheepPointer_ data member (a pointer to a sheep).

// Approach 1: Set variables in constructor body (not preferred)
Barn::Barn(string name,
           size_t age) {  // sheep_ and sheepPointer_ are default initialized
    // sheep_ and sheepPointer_ are updated to the intended value in the use
    // phase
    sheep_ = Sheep(name, age);
    sheepPointer_ = new Sheep(name, age);
    sheep_.feed();
    sheepPointer_->feed();
}

// Approach 2: Initialize variables in member initialization list (preferred)
Barn::Barn(string name, size_t age)
    :  // sheep_ and sheepPointer_ are initialized with the intended value
      sheep_{name, age},
      sheepPointer_{new Sheep(name, age)} {
    // Anything in the use phase still goes in the body of the constructor
    sheep_.feed();
    sheepPointer_->feed();
}

Notice that in Approach 1, we default initialized sheep_, meaning that we called the Sheep default constructor. If the Sheep class did not have a default constructor, it would have caused an error! This is another advantage of the member initialization list — it allows us to initialize data members that do not have a default constructor.

In summary, the only reasons not initialize a member variable in the member initialization list are:

  • It already had a perfectly good explicitly specified initialization in the class definition and we don't need to override that initialization with anything else.
  • It is an array that needs to be initialized via a loop to set its members to specific values.

Absent one of these reasons, always initialize member variables in the member initialization list.

(In very rare cases, it's not possible to know the right value to initialize a member to until after the constructor body has started executing. In that case, we need to overwrite the initial value with a new one in the constructor body. But we don't expect such cases to come up in this class, so check with a grutor or instructor if you think you need to do this.)

4.2 Avoid unnecessary this keywords

When you are inside of an object in C++, the this keyword is a pointer to the object. In other languages like Java and C#, it is idiomatic to use always use the this keyword when referring to member methods and variables. However, in C++, the idiom is to only use the this keyword when necessary, and not when referring to member methods and variables.

In the following example, both of the this keywords are superfluous; we should simply write insert(length_).

// Example of unnecessary this keywords (should be removed)
this->insert(this->length_)

However, when you need a pointer to the current object, you should use the this keyword. The following if statement checks if the object we are inside of is equal to rhs. Notice that we have to deference this because it is a pointer.

// Example of necessary this keywords (should not be removed)
if (*this == rhs)

Similarly, if you want to see if our object is the exact same object as rhs, write:

// Example of necessary this keywords (should not be removed)
if (this == &rhs)

4.3 Label methods const when applicable

When we label a member method as const, it promises that the method will not change any of the data members of the object. It allows us to call that method on const instances of the object (such as a const reference to the object). We should always label a member method as const if it does not change any data members.

We need to label both the method declaration in the .hpp and the method definition in the .cpp. For example, suppose that our class has a size() member method which returns the private data member size_ without modifying it.

// In the .hpp:
size_t size() const;

// In the .cpp:
size_t size() const {
    return size_;
}

4.4 Implement != with ==

If a class supports operator== and operator!=, they should always return the opposite value (if a == b evaluates to true, then a != b should evaluate to false, and vice versa). To avoid code duplication, we should always implement != by calling == and negating the result. Here is an example for the Sheep class.

bool operator!=(const Sheep& other) const {
    return !(*this == other);
}

We use *this to get a reference to ourself since this is a pointer.

4.5 Prefer synthesized methods when applicable

The compiler can create synthesized versions of the following methods:

  1. Default Constructor (Class() = default): The synthesized default constructor default constructs all private data members.
  2. Copy Constructor (Class(const Class& other) = default): The synthesized copy constructor copy constructs each private data member parameterized with the corresponding data member in other.
  3. Assignment operator (Class& operator=(const Class& other) = default): The synthesized assignment operator calls the assignment operator on each private data member parameterized with the corresponding data member in other.
  4. Destructor (~Class() = default): The synthesized destructor calls the destructor on each private data member.

If any of these synthesized methods have the desired behavior, we should use the synthesized version by writing = default in the .hpp file rather than explicitly implementing the equivalent method in the .cpp file.

For example, consider the following class.

// sheep.hpp
class Sheep {
 public:
    Sheep();
    Sheep(const Sheep& other);

 private:
    list<Sheep*> friends;
};

Suppose that we implemented the default and copy constructors as follows.

// sheep.cpp
Sheep::Sheep() : friends_{} {
    // Nothing left to do
}

Sheep::Sheep(const Sheep& other) : friends_{other.friends_} {
    // Nothing left to do
}

For both constructors, we implemented the exact behavior as the synthesized version! Thus, we should remove these implementations from sheep.cpp and simply use the synthesized version in sheep.hpp.

// sheep.hpp (using synthesized constructors)
class Sheep {
 public:
    Sheep() = default;
    Sheep(const Sheep& other) = default;

 private:
    list<Sheep*> friends;
};

// sheep.cpp no longer implements Sheep() or Sheep(const Sheep& other)

4.6 Avoid explicitly calling operators and destructors

In C++, we can define how certain symbols (such as * and ==) act on objects of our class by implementing the corresponding operator method (such as operator* and operator==). The operator method is then called whenever we use the corresponding symbol on the object. For example, a == b will call operator== of a with b as the parameter. We should always call operators by using the corresponding symbol rather than explicitly calling the operator. For example, we should not write a.operator==(b), even though it has the exact same behavior as a == b.

Here are some examples which explicitly and implicitly use different operators.

// Example 1: operator==
Sheep shawn, elliot;
std::cout << shawn.operator==(elliot) << std::endl;  // explicit use (not preferred)
std::cout << shawn == elliot << std::endl;           // implicit use (preferred)

// Example 2: operator=
shawn.operator=(elliot);  // explicit use (not preferred)
shawn = elliot;           // implicit use (preferred)

// Example 3: operator*
vector<size_t> vec = {1, 2, 3}
std::cout << vec.begin().operator*() << std::endl;  // explicit use (not preferred)
std::cout << *vec.begin() << std::endl;             // implicit use (preferred)

Similarly, we should never explicitly call an object’s destructor. An object’s destructor is automatically called in the destruction phase of the object’s lifetime.

Sheep* timmy = new Sheep{};
timmy->~Sheep();  // explict destructor call (not preferred)
delete timmy;     // the Sheep's destructor is implicitly called when we delete timmy

5. Iterators

5.1 Use standard template aliases

When creating an iterator, we should include certain aliases so that it can interface with the Standard Template Library (STL). The following example shows these aliases for a forward iterator over a data structure containing strings (such as the iterator for TreeStringSet).

using value_type = std::string;
using reference = value_type&;
using pointer = value_type*;
using iterator_category = std::forward_iterator_tag;

Our iterator implementation and interface should uses these aliases rather than the explicit types whenever applicable. For example, consider the declaration of operator* and operator-> for this iterator.

// Approach 1: Explicit types (not preferred)
string& operator*() const;
string* operator->() const;

// Approach 2: STL aliases (preferred)
reference operator*() const;
pointer operator->() const;

5.2 Implement operator-> with operator*

For an iterator, operator* returns the value to which the iterator currently points and operator-> returns the address of that value. Because the result of operator-> is directly dependent on the result of operator*, we should always implement operator-> for iterators by leveraging operator*.

Suppose that we are writing the iterator for the Foo class. We can implement operator-> as follows.

Foo::Iterator::pointer operator->() const {
    return &(**this);
}

To understand what &(**this) means, let’s work inside out.

  • this: A pointer to the iterator on which we called operator->.
  • *this: Dereferences this to give the iterator itself.
  • **this: Calls operator* on the iterator, which returns the value to which the iterator currently points.
  • &(**this): Gives the address of the value returned by **this.

(When logged in, completion status appears here.)