Chapter 2

Philosophy

Images

Cippi thinks deeply.

The philosophical rules have a general focus and are, therefore, not checkable. The philosophical rules provide the rationale for the following concrete rules. Due to the fact that there are only 13 philosophical rules, I can cover all of them in this chapter.

P.1

Express ideas directly in code

A programmer should express their ideas directly in code because code can be checked by compilers and tools. The two following methods make this rule obvious.

class Date {
   // ...
public:
  Month month() const; // do 
  int month(); // don't
  // ...
};

The second member function month() expresses neither that it is constant nor that it returns a month. The same argument typically holds for loops such as for or while compared to the algorithms of the Standard Template Library (STL). The next code snippet makes my point.

int index = -1;                                   // bad
for (int i = 0; i < v.size(); ++i) {
   if (v[i] == val) {
      index = i;
      break;
   }
}
 

auto it = std::find(begin(v), end(v), val);    // better

A professional C++ developer should know the algorithms of the STL. By using them, you avoid the usage of explicit loops, and your code becomes easier to understand, easier to maintain, and therefore, less error prone. There is a proverb in modern C++: “When you use explicit loops, you don’t know the algorithms of the STL.”

P.2

Write in ISO Standard C++

Okay, this rule is a no-brainer. To get a portable C++ program, the rule is quite easy to understand. Use a current C++ standard without compiler extension. Additionally, be aware of undefined or implementation-defined behavior.

When you have to use extensions that are not written in ISO Standard, encapsulate them in a stable interface.

P.3

Express intent

What intent can you derive from the following implicit and explicit loops?

for (const auto& v: vec) { ... }                               // (1)
 
for (auto& v: vec) { ... }                                     // (2)
 
std::for_each(std::execution::par, vec, [](auto v) { ... });  // (3)

Loop (1) does not modify the elements of the container vec. This does not hold for the range-based for loop (2). The algorithm std::for_each (3) performs its job in parallel (std::execution::par). This means that we don’t care in which order the elements are processed.

Expressing intent is also an important guideline for good documentation of your code. Documentation should state what should be done and not how it should be done.

P.4

Ideally, a program should be statically type safe

C++ is a statically typed language. Statically typed means that the type of the data is known to the compiler. Statically type safe additionally states that the compiler detects type errors. Due to existing problematic areas, this goal cannot always be achieved, but there is a cure for unions, casts, array decays, range errors, or narrowing conversions:

int i1(3.14);
int i2 = 3.14;

The compiler detects narrowing conversion if you use the { }-initializer syntax.

int i1{3.14};
int i2 = {3.14};

P.5

Prefer compile-time checking to run-time checking

What can be checked at compile time should be checked at compile time. This is idiomatic for C++. Since C++11, the language has supported static_assert. Thanks to static_assert, the compiler evaluates an expression such as static_assert(size(int) >= 4) and produces, eventually, a compiler error. Additionally, the type-traits library allows you to formulate powerful conditions: static_assert(std::is_integral<T>::value). When the expression in the static_assert call evaluates to false, the compiler writes a human-readable error message.

P.6

What cannot be checked at compile-time should be checkable at run-time

Thanks to the dynamic_cast, you can safely convert pointers and references to classes up, down, and sideways along the inheritance hierarchy. If the casting fails, you get back a nullptr in case of a pointer and a std::bad_cast exception in case of a reference. Read more details in the section dynamic_cast in Chapter 5.

P.7

Catch run-time errors early

Many countermeasures can be taken to get rid of run-time errors. As a programmer, you should take care of pointers and C-arrays by checking their range. Of course, the same holds for conversions, which should be avoided if possible, and of course, for narrowing conversions. Checking input also falls into this category.

P.8

Don’t leak any resources

Resource leaks are, in particular, critical for long-running programs. A resource may be memory but also file handles or sockets. The idiomatic way to deal with resources is RAII. RAII stands for Resource Acquisition Is Initialization and means, essentially, that you acquire the resource in the constructor and release the resource in the destructor of a user-defined type. By making the object a scoped object, the C++ run time automatically takes care of the lifetime of the resource. C++ uses RAII heavily: Locks take care of mutexes, smart pointers take care of raw memory, or containers of the STL take care of the underlying elements.

P.9

Don’t waste time or space

Saving time or space is a virtue. The reasoning is quite concise: This is C++. Do you spot the issues in the following loop?

void lower(std::string s) {
   for (unsigned int i = 0; i <= std::strlen(s.data()); ++i) { 
      s[i] = std::tolower(s[i]);
   }
}

Using the algorithm std::transform from the STL makes a one-liner out of the previous function.

std::transform(s.begin(), s.end(), s.begin(), 
              [](char c) { return std::tolower(c); });

In contrast to the function lower, the algorithm std::transform automatically determines the size of its string. Consequently, you don’t have to specify the length of the string using std::strlen.

Here is another typical example, often found in production code. Declaring copy semantics (copy constructor and copy-assignment operator) for a user-defined data type suppresses the automatically defined move semantics (move constructor and move-assignment operator). Ultimately, the compiler can never use cheap move semantics if applicable but always relies on expensive copy semantics.

struct S {
   std::string s_;
   S(std::string s): s_(s) {}
   S(const S& rhs): s_(rhs.s_) {}
   S& operator = (const S& rhs) { s_ = rhs.s_; return *this; }
};
 
S s1;
S s2 = std::move(s1); // makes a copy instead of moving from s1.s_

If these examples scare you, read more in the section Default Operations in Chapter 5.

P.10

Prefer immutable data to mutable data

There are many reasons to use immutable data. First, it is easier to verify your code when you use constants. Constants also have higher optimization potential. But first and foremost, constants provide a big advantage in concurrent programs. Constant data is data-race free by design because mutation is a necessary condition for a data race.

P.11

Encapsulate messy constructs, rather than spreading through the code

Messy code is often low-level code, which hides bugs and is, therefore, error prone. If possible, replace your messy code with a high-level construct from the STL such as a container or algorithms of the STL. If this is not possible, encapsulate the messy code in a user-defined type or a function.

P.12

Use supporting tools as appropriate

Computers are better than humans at doing boring and repetitive tasks. That means that you should use static analysis tools, concurrency tools, and testing tools to automate these verifying steps. Compiling your code with more than one C++ compiler is often the easiest way to verify your code. An undefined behavior that may not be detected by one compiler may cause another compiler to emit a warning or produce an error.

P.13

Use support libraries as appropriate

That is quite easy to explain. You should go for well-designed, well-documented, and well-supported libraries. You will get a well-tested and nearly error-free library and highly optimized algorithms from the domain experts. Outstanding examples are the C++ standard library, the Guidelines Support Library, and the Boost libraries.