C++ is awesome, here's why...
Vinit Jogani
Posted on July 24, 2021
C++ is, hands down, one of the most misunderstood languages in software developer pop culture today. People often compare it to C because it is a "low-level" language. Consequently, it has received a reputation of being an esoteric language that only the performance paranoid care about. This is far from true. I have been programming in C++ as my primary language for a while now and the developer experience is actually really good -- much better than I had imagined it would be.
In this article, I want to debunk some common myths about C++ that I heard before I started using it. Then, I want to talk about the actual superpowers that C++ provides you that most other languages do not.
Myth 1: memory this, memory that
We all know that C is infamous for manual memory management e.g. with the malloc
and free
. They are hard to work with, lead to many error conditions that need to manually be checked for, and is overall a nightmare. When people hear that C++ also has great performance, people assume that it is by dabbling into all the specifics of memory allocation much like in C so they conclude that it would also be a nightmare. This is categorically false.
For a while now, C++ has had smart pointers. Using these, you can get the same behavior as you get with objects in other languages like Java and Python. In particular, the std::shared_ptr
works by wrapping a regular object in a copy-able and movable object with a reference counting mechanism. Thus, when no code is referencing a shared_ptr
, it is safely destructed and freed like in most languages. The simplest way to construct a shared pointer is as follows:
std::shared_ptr cat(new Pet("cat"));
// or
std::shared_ptr cat = std::make_shared<Pet>("cat");
While this is the common pattern in most other languages, C++ allows you to have further control on how an object is accessed e.g. using a unique_ptr
, but more on that later.
Overall, with smart pointers, managing memory in C++ is no harder than any other languages. It is, however, intentional in that you need to clarify that this is your expected behavior because you can still create and pass around regular pointers the good (ugly?) old way.
Typically, with good use of these, you are also very unlikely to run into segmentation faults which were oh-so-common in C.
Myth 2: it is old and outdated
C++ is very actively maintained with new features that continue to be rolled out at a regular basis. I think one of the most common "new" features in many languages that people have grown to admire is lambdas. Surely, C++ doesn't have lambdas right? Wrong. A C++ lambda function can be defined as:
auto square = [=](int x) { return x * x; };
For context, Java got lambdas in 2014 with the release of Java 8. C++ has had lambdas since C++11 (2011). Yep.
And there continue to be major updates e.g. C++20 most recently, that introduce even more features to make C++ easier to work with. For example, variable-length arguments have been in C++ for a while with variadic templates. Generics work great in C++ as well, although differently from what you may be used to in Java. These features continue to improve the way we develop software.
Myth 3: it is easy to get wrong
On the contrary, while learning some of its quirks may take a while, C++ makes it very hard for your code to do undesired things. For example, many object-oriented languages do not have support for "pure" functions i.e. functions that are guaranteed to be immutable. In C++, you can actually mark methods of a class as const
if they don't modify the class' state. These methods can then also be called on constant instances of the class. Here's an example:
class Greeting {
public:
Greeting(std::string greeting) : greeting_(greeting) {}
std::string get_greeting() const {
return greeting_;
}
std::string set_greeting(std::string new_) {
greeting_ = new_;
}
private:
std::string greeting_;
};
Now, you can initialize this class as a constant and still call the getter. If you try to mutate the state of the class, the compiler will complain!
const Greeting g("hello");
g.get_greeting(); // returns "hello"
g.set_greeting("hi"); // does not compile
To be accurate, these functions are not fully pure in that if you don't type some of your variables correctly, it is possible to mutate the resources. For example, if you have a const
pointer to a non-const
object, you may not modify the pointer but can modify the object pointed to by the pointer. However, these problems can typically be avoided by typing the pointer correctly (i.e. making it a const
pointer to a const
object).
It may seem like I am self-contradicting here by mentioning such an edge case in a common use case. However, I don't think it really contradicts what my larger claim here is: C++ makes it hard to go wrong assuming you know what you want by giving you the tools to express exactly that in code. While programming languages like Python might abstract it all away from you, they come at a much higher cost. Imagine going into an ice cream shop and only having chocolate served to you because that's what most people generally want -- that's Python. Depending on how you look at it, sure in some sense it is harder to go wrong with chocolate, in general it is not upto the shop but the user to know what they need.
Good const
-ness is a big plus but there's several other things that C++ allow you to do that prevent production bugs from occurring in larger projects. It allows you to configure move/copy/delete semantics for the classes you design if you need. It allows you to pass things by value and advanced features like multiple inheritance. All these things make C++ less restrictive.
Myth 4: it is verbose
Okay, this is not completely inaccurate. Coming from Python, where folks are so tired of typing numpy
that they just collectively all decided to import it as np
, typing more than 2 letters does feel verbose. But modern C++ is a lot less verbose than it used to be! For example, type inference like Go is available in C++ with the introduction of the auto
keyword.
auto x = 1;
x = 2; // works
x = "abc"; // compilation error
You can also use auto
in return types:
auto func(int x) { return x * x; }
You can also use it in for loops to loop over maps for example:
for (auto& [key, value]: hashmap) {...}
auto
does not mean that the types are dynamic -- they are still inferred at compile time and once assigned, they cannot be changed. This is probably for the best. In practice for large codebases, it arguably also helps readability to specifically type out the full type instead of using auto
. However, the feature is there if you need it.
You can also specify type and/or namespace aliases like in Python. For example, you can do something like:
using tf = tensorflow;
// Now you can use tf::Example instead of tensorflow::Example.
C++ templates (similar to Java Generics, but pretty different at the same time) can also help significantly cut down duplicate code and may be an elegant solution for many use cases.
Overall, C++ is definitely more verbose than many new programming languages like Kotlin and Python. However, it is not a lot more verbose than C# or Java or even JavaScript to some extent, and the verbosity of those languages has not affected their popularity too much.
Myth 5: hard to do common things
Again, this is not completely inaccurate. Common operations like joining strings by a delimiter are more complicated than they need to be. However, this is a problem solved rather easily with open source libraries like Google's Abseil with thousands of utility functions that make it very easy to work with. Apart from string utilities, Abseil also contains special implementations of hashmaps, concurrency helpers, and debugging tools. Other libraries like Boost make it easy to work with BLAS routines (e.g. dot products, matrix multiplications, etc.) and are very performant.
Using libraries itself can be a challenging task in C++ with having to maintain CMake
files, although in many ways those are not much different from the gradle
or package.json
files in other languages. However, again Google's open source build tool Bazel makes it extremely easy to work with even with cross-language builds. It takes some getting used to, but provides really quick builds and in general has a very good developer experience.
Superpowers
So after busting all those common myths about C++, here are some things in C++ that a lot of other languages don't allow you to do:
Customize semantics of your classes
Suppose you have a class that contains some large data. You would prefer it not be copied and instead be passed by reference always. You can enforce this as a designer of the interface. Moreover, you can configure exactly how you would like it to be copied if at all it is required.
What about moving objects -- instead of copying all data from the previous block of memory to a new block and then deleting the old block of memory, maybe you can optimize it by just switching the pointer locations.
What about destroying objects -- when a object goes out of scope, maybe you automatically want to release some resources (e.g. think mutexes that are automatically released at the end of the function). This works much like the defer
functionality in Go.
As a designer of an interface, you can customize every small aspect of how users will use your class. In most cases, this is not necessary but when it is, C++ allows you to fully express your requirements. That is very powerful and can save your company hours of time on large projects.
Optimize memory access
I briefly mentioned smart pointers above. Apart from shared_ptr
, you can also have a unique_ptr
that ensures that only one object can own a resource. Having one owner for data makes it easy to organize and reason about large projects. In fact, while shared_ptr
most closely mimics Java and other OOP languages, you're generally better off using a unique_ptr
. This also adds to the safety of the language.
You can also specify compile-time constants to enable the compiler to do more work during the build so that the binary runs faster overall.
Strong types
In general, I have found that working with typed objects is SO much easier to debug than other languages. For example, after hours of debugging a JavaScript project, I found that the error arose because I was passing in 1 argument to a 2 argument function. JavaScript threw no errors and just produced undesired outputs. I would much rather have something fail to compile than fail during runtime.
However, there are many typed languages out there so it may seem overkill to use this as a super power. But C++ does more. First of all, C++ lets you pass anything by reference, by pointer or by value. That means you can pass in a reference to an integer and have a function mutate it instead of using the return value (might be handy in some cases). It also means that you can perform memory-level operations using a pointer on anything if need be. Usually, though, you would like to pass things as a constant reference (e.g. const A&
) which does not lead to a copy and keeps your object safe from being mutated unintentionally. Those are strong guarantees to have and that makes your code just so much easier to reason about. In typescript, for example, you cannot reassign a const
object but you can mutate it. Why even -- just uggh.
Caveats
So C++ is great and all, but there are obvious limitations to what I would use it for. You can write microservices (e.g. in gRPC) rather easily but I would likely not use C++ for the actual web server (I would likely use TypeScript for that). Despite its speed, I would likely not use C++ for data analysis (I would likely use pandas
for that). There are some things that C++ is great for and there are other things that it's just not suitable for. Ultimately, you still have to choose the right tool for whatever job you're trying to accomplish. Having said that, hopefully this article made C++ a bit more attractive in your eyes than you're used to seeing it as.
Posted on July 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.