Modern C++: A Love/Hate Story
From an Intermediate’s Point of View
It’s not just about performance!
Recently, I more frequently hear many say “C++ is just about performance!”. Is that it? I mean. It’s great to learn a performant¹ programming language, but is it enough to prefer one language over another? Then, why not C, for example. For me, personally, C++ is more than that—so much more.
For some C++ developers, it’s somehow like being part of a cult or a religion. The matter, for many others, is that they love C++, but they cannot give a reason why. I used to have the same problem, until I began to realize that there are many good reasons to love C++, including:
- It’s, of course, a very performant language. Not only because it compiles into machine code or STL is highly optimized, but it’s due to the fact that modern C++ compilers are getting so smart that optimized C++ codes can easily compete expert C codes in speed.²
- It’s concise, yet productive. One of the most beautiful concepts in C++ is that anything other than primitive types is just a
class. There are no
abstractclasses in a particular way.³ All the varieties in abstract data types come from a single keyword,
- It’s powerful. I’m not talking about a low-level programming language that gives you the ultimate power of pointers. It’s a general purpose language equipped with all the tools you need to express what’s in your mind. If you believe that C++ is only good for system programming, just take a look at GitHub projects to see the broad range of fields that it has been used for.
- It has a nice interaction with the OS, since all widely used OS’s are written in C or C++. It allows to utilize system resources more efficiently.
- It’s a mature programming language. There are so many (mostly open source) libraries around, and it has a large community that its members humbly provide free tutorials and answers to your questions.
But the story does not end here.
You don’t know C++!
Before moving forward, I’d like to suggest you to read “C++ Common Knowledge”, an excellent book written by Stephen Dewhurst about common idioms and mistakes about modern C++. Somewhere in the Preface, he states,
… I was engaged in conversation with a group of well-known C++ experts at a conference. There was a general pall or depression among these experts that modern C++ is so complex that the “average” programmer can no longer understand it.
So, the experts blame it for being too complex and not understandable. However, I think the language is not guilty by itself, but it’s becuase often there’s more than just one way to solve a problem in C++ and unexperienced programmers usually get confused about which one is the best⁵. For example, to implement Strategy pattern, should someone use policy-based design, or inheritance, or lambda functions, or even function pointers? It’s a lack of knowledge about how to use language features.
I don’t intend to go too deep about design problems here⁶, but in the following sections, I’m going to address some of the most problematic idioms that every C++ programmer needs to concern in his career.
Templates are not templates!
One of the major differences between C++ and the other OO programming languages is that templates in C++ are not basically codes. The standard specifies that the templates are instantiated⁷ by the compiler just when they’re first used. Not understanding C++ templates and using them like the way they work in C# or Java, put the programmer in a big problem.
This odd principle has made templates one of the most poweful features of the language. Templates make possible to write generic codes, as other OO programming languages does, and also, allow another advanced feature of the language, called meta programming. However, meta programming requires expertise and a good understanding of other principles like SFINAE.⁸
Also, it must be noted that meta programming is a technique that should not be overused. First, it makes the code hard to understand and can cause more hurt rather than benefit. Second, it can easily push the compiler to its limits by significantly increasing memory usage and compile time.
Beware of raw pointers!
Nowadays, most modern programming languages benefit from a garbage collector at their runtime system. Garbage collectors are responsible for managing allocated memory for reference types, i.e., class objects. C++ lacks a garbage collector and uses a more advanced principle called RAII, instead.⁹
In the garbage collected OO languages, objects are typically constructed with a
new keyword (or a similar approach), and the programmer doesn’t need to worry about the object destruction. Although C++ supports manual object lifetime management using
delete keywords, but it’s considered a very bad practice. Also, in C++, there’s no point to allocate an object on the heap, only when,
- The object size is unknown at first, such as arrays and vectors.
- The object is too large to be allocated on the stack.
- The object is intended to be passed to another thread in concurrent applications — however, there are workarounds to avoid it.
- Someone needs to use runtime polymorphism, as unfortunately C++ requires object pointers for that.
Having said that, the programmer is encouraged to use
make_shared functions for the heap allocation, whenever needed.
STL is a part of the language!
Although it’s commonly misbelieved, STL is not just a collection of data structures and algorithms written by expert compiler constructors — not anymore. In modern C++, the language is getting more and more tied to it.¹⁰
Just take a look at the standards. In C++11, there’s a strict connection between range-based
for loops and iterators, and also, uniform initialization and
initializer_list. And, there will be more with upcoming standards. So, the programmer is intended to learn STL well and use it regularly.
Avoid virtual as much as you can!
When I was reading the well known GoF book, “Design Patterns”, I was more curious about a single phrase, “Prefer composition over inheritance.” Well, hopefully it must be just a design decision, I thought. But today, I see that virtual polymorphism is the most hated feature of the C++ language — and it’s not hard to understand why. First, it works only on object pointers, and second, the virtual function call is very slow, compared to the direct function call.¹¹
The rages over virtual functions are so high that I’d expect a big revolution someday. But till that day, I suggest to avoid them as much as you can. There are a handful of techniques to extend class behaviors, such as policies, CRTP, and also composition. The good thing about the composition is that it works in runtime. So, it’d be better to prefer composition over inheritance when runtime polymorphism is required.
Functions are not evil!
C++ is rather a multi-paradigm language than just a pure OO language. You are free to use structural, object-oriented and functional paradigms at the same time. So, when you simply need a function, just define a function and don’t bother encapsulating it in a class as it’s common in other fully OO languages, like Java. Use namespaces to categorize functions whenever needed.
Actually, in my experience, functions and lambda functions can even lead to a clearer design in many applications which require composable behaviors, and the type deduction works more intuitively with function templates.
Learn type deduction rules; and learn them well!
Every C++ programmer is pretty familiar with function overloading and determining which one’s called against passed arguments, I guess. The type deduction is very similar for template parameters and
auto keyword, except that they obey more complex rules.¹²
Without having a good understanding of the type deduction rules, it’s almost impossible to understand modern C++ anymore. Besides, with C++14 and C++17 standards being published, these rules are increasingly important to understand.
const and final; they’re not just decorators!
Anybody can remember the time when macros were very popular for defining constants. It was believed that inlining feature of the macros makes the code a lot faster. But modern compilers are way smarter than before, right?
const you give the compiler a hint that the value (or the reference) is not meant to be changed, so that the compiler can generate more efficient codes treating it. But it’s not the end of the story. It also prevents unexpected behaviors caused by sudden changes, which becomes more important when writing concurrent applications. So, do consider using
const, especially when handing over a reference to a class member. And by the way, try not to use
const_cast for the same reason.¹³
final keyword is very much like
const, except that it prevents a class to be inherited or a virtual function to be overridden by child classes. Its usage might be a little less obvious, but use it wherever appropriate.
The less known
mutable keyword is also important to be noted here, because it behaves differently when used for class members and lambda functions. When used with a class, it denotes that a field doesn’t affect the external visible state of the class, which simply means that it’s safe to modify a
mutable field without worrying about changing the object’s behavior, even if it’s defined as
const. On the other hand, decorating a lambda function, the
mutable keyword removes the
const qualification from parameters captured by copy.
Give the ownership away!
Move semantics are one of the revolutionary changes in C++. Both move constructors and move operators eliminate a lot of unnecessary copies during assignments and passing arguments, leading to a more efficient code. Though, learning the the difference between lvalue and rvalue concepts might be a little confusing at first, but it’s worth it.
However, move semantics are not just about optimization. They really come in handy when dealing with concurrency. Keeping shared states is one of the biggest challenges in writing concurrent applications as it requires concerning mutex, dead lock, etc. It’d be a lot easier to make a copy or hand the ownership over with move semantics if it’s not needed anymore.
C++ language is evolving very rapidly that makes it hard to follow all the changes up. This article tries to address some of the significant issues that most programmers are dealing with them about modern C++, but it’s not nearly enough. Any programmer should take his/her time to read excellent textbooks provided by experts — some of them referenced here—to fully understand the right way to write a software in C++.
¹ Some English language speakers may not like word performant, but I’m using it because it’s acceptable among software developers.
² See Matt Godbolt’s talk at CppCon 2017, “What Has My Compiler Done for Me Lately? Unbolting the Compiler’s Lid”
³ Think of
struct as a
class that all its members are
public by default. Also, an
interface is just a
class that contains no data members and all its methods are
public and pure
⁴ I don’t mind
union, because it’s gradually getting depracated when the time goes by. It’s one of the C left-overs from the old past.
⁵ The word best is somewhat ambiguous here. It commonly means fastest execution among developers, but also clearest and most acceptable.
⁶ There are some nice literatures around, e.g., “Modern C++ Design” by Andrei Alexandrescu, “Effective Modern C++” by Scott Meyers, and the Stephen Dewhurst’s book.
⁷ Template instantiation is not analogous to class instantiation. It means the compiler generates the actual code for a template.
⁸ For more information read “C++ Templates” by David Vandevoorde, and Nicolai Josuttis.
⁹ There are C++ libraries that mimic garbage collection, but as I said, it’s not a part of the language or its runtime.
¹⁰ Actually, I hate it, but this is the way it works.
¹¹ Calling a virtual function requires two memory lookups. First to load virtual table, and second to load virtual function’s address. However, the cost of virtual functions isn’t really the lookup of a function pointer in the virtual table, but that the indirect jump usually cannot be branch-predicted.
¹² See Scott Meyers’s talk at CppCon 2014, “C++ Type Deduction and Why You Care”
¹³ On the other hand,
volatile is the most misinterpreted keyword in C++. The life would be easier if you forget about it completely.