Why C++?

Learn about and practice some of the features of C++ that are important for writing robust, high-performance applications.

Why should we use C++?

Let’s begin by exploring some of the reasons for using C++ today. In short, C++ is a highly portable language that offers zero-cost abstractionsDisplaying only essential information and hiding the details.. Furthermore, C++ provides programmers with the ability to write and manage large, expressive, and robust code bases. In this chapter, we’ll look at what we mean by zero-cost abstractions, compare C++ abstraction with abstraction in other languages, and discuss portability and robustness, and why such features are important. Let’s begin by getting into zero-cost abstractions.

Zero-cost abstractions

Active code bases grow. The more developers work on a code base, the larger the code base becomes. In order to manage the growing complexity of a code base, we need language features such as variables, functions, and classes to be able to create our own abstractions with custom names and interfaces that suppress details of the implementation.

C++ allows us to define our own abstractions, but it also comes with built-in abstractions. The concept of a C++ function, for example, is in itself an abstraction for controlling program flow. The range-based for-loop is another example of a built-in abstraction that makes it possible to iterate over a range of values more directly. As programmers, we add new abstractions continuously while developing programs. Similarly, new versions of C++ introduce new abstractions to the language and the standard library. But constantly adding abstractions and new levels of indirection comes at a price—efficiency. This is where zero-cost abstractions play their role. A lot of the abstractions offered by C++ come at a very low runtime cost with respect to space and time.

With C++, we are free to talk about memory addresses and other computer-related low-level terms when needed. However, in a large-scale software project, it is desirable to express code in terms that deal with whatever the application is doing and let the libraries handle the computer-related terminology. The source code of a graphics application may deal with pencils, colors, and filters, whereas a game may deal with mascots, castles, and mushrooms. Low-level computer-related terms, such as memory addresses, can stay hidden in C++ library code where performance is critical.

Programming languages and machine code abstractions

In order to relieve programmers from the need to deal with computer-related terms, modern programming languages use abstractions so that a list of strings, for example, can be handled and thought of as a list of strings rather than a list of addresses that we may easily lose track of if we make the slightest typo. Not only do the abstractions relieve the programmers from bugs, but they also make the code more expressive by using concepts from the domain of the application. In other words, the code is expressed in terms that are closer to a spoken language than if expressed with abstract programming keywords.

C++ and C are two completely different languages nowadays. Still, C++ is highly compatible with C and has inherited a lot of its syntax and idioms from C. To give you some examples of C++ abstractions, we will show how a problem can be solved in both C and C++.

Take a look at the following C/C++ code snippets, which correspond to the question: "How many copies of Hamlet are in this list of books?"

Note: For the sake of brevity, we have hidden the testing logic in this lesson. As we move through the course, we will see instances of test cases displayed alongside the code snippets.

We will begin with the C version:

Press + to interact
// C version
struct string_elem_t { const char* str_; string_elem_t* next_; };
int num_hamlet(string_elem_t* books) {
const char* hamlet = "Hamlet";
int n = 0;
string_elem_t* b;
for (b = books; b != 0; b = b->next_)
if (strcmp(b->str_, hamlet) == 0)
++n;
return n;
}

The equivalent version using C++ would look something like this:

Press + to interact
// C++ version
auto num_hamlet(const std::forward_list<std::string>& books) {
return std::count(books.begin(), books.end(), "Hamlet");
}

Although the C++ version is still more of a robot language than a human language, a lot of programming lingo is gone thanks to the higher levels of abstraction. Here are some of the noticeable differences between the preceding two code snippets:

  • The pointers to raw memory addresses are not visible at all.

  • The std::forward_list<std::string> container replaces the handcrafted
    linked list using string_elem_t .

  • The std::count() the function replaces both the for-loop and the if-statement.

  • The std::string class provides a higher-level abstraction over char* and strcmp() .

Basically, both versions of num_hamlet() translate to roughly the same machine code, but the language features of C++ make it possible to let the libraries hide computer-related terminology such as pointers. Many of the modern C++ language features can be seen as abstractions on top of basic C functionality.

Abstractions in other languages

Most programming languages are based on abstractions, which are transformed into machine code to be executed by the CPU. C++ has evolved into a highly expressive language, just like many of the other popular programming languages of today. What distinguishes C++ from most other languages is that while the other languages have implemented these abstractions at the cost of runtime performance, C++ has always strived to implement its abstractions at zero cost at runtime.

This doesn’t mean that an application written in C++ is by default faster than the equivalent in, say, C#. Rather, it means that by using C++, you’ll have fine-grained control of the emitted machine code instructions and memory footprint if needed.

To be fair, optimal performance is very rarely required today, and compromising performance for lower compilation times, garbage collection, or safety, as other languages do, is in many cases more reasonable.

The zero-overhead principle

"Zero-cost abstractions" is a commonly used term, but it is afflicted with a problem—most abstractions usually do cost. If not while running the program, it almost always costs somewhere down the line, such as long compilation times, compilation error messages that are hard to interpret, and so forth. What is usually more interesting to talk about is the zero-overhead principle. Bjarne Stroustrup, the inventor of C++, defines the zero-overhead principle like this:

  • What you don't use, you don't pay for

  • What you do use, you couldn't hand code any better

This is a core principle in C++ and a very important aspect of the evolution of the language. Why, you may ask? Abstractions built on this principle will be accepted and used broadly by performance-aware programmers and in a context where performance is highly critical. Finding abstractions that many people agree on and use extensively makes our code bases easier to read and maintain.
On the contrary, features in the C++ language that don't fully follow the zero-overhead principle tend to be abandoned by programmers, projects, and companies. Two of the most notable features in this category are exceptions (unfortunately) and Run-time Type Information (RTTI).

Both these features can have an impact on the performance even when they are not being used. We strongly recommend using exceptions, though, unless you have a very good reason not to. The performance overhead is, in most cases, negligible compared to using some other mechanism for handling errors.