A Practical Subset of C++ Features Used at Work
This article summarizes a practical subset of C++ features commonly used in systems programming, including recommendations on RAII, smart pointers, move semant…
I have been using C++ for quite a few years, so I am summarizing a practical subset of C++ features that I commonly use at work. The primary use case here is systems programming, excluding various general-purpose libraries.
Advanced C
In practice, I mostly use C++ as a more advanced C language. Compared with C, C++ provides the following useful features:
RAII
Smart Pointers
Move Semantics
Namespace
Access Control (private, protected & public)
Generics (Template)
Structured Exception Handling
Lambda Expression
Enum class
Smart pointers allow C++ to automatically manage object lifetimes to some extent. RAII lets us associate the allocation and release of arbitrary resources with object lifetimes. Together, they allow us to automatically manage resource lifetimes to some extent. This makes C++ a modern development language that can compete with Python and Java.
RAII binds resource management to object lifetimes, but sometimes we need to temporarily break that binding and then bind them again. At that point we need move semantics, which provides the ability to hand off resource ownership.
Compared with Namespace + Access Control, what I actually want more is Module, but this proposal has also been delayed for a long time [1]. After we have Module, I also want one piece of syntactic sugar: extension methods. That is, functionality where f(obj, …) is equivalent to obj.f(…). This way, some methods would not necessarily have to be written inside a class.
It is excellent that C++ has generics, but there are still two drawbacks. One is that all generic code must be written in header files, which is also a defect caused by the lack of Module [1]. The other is that there is no good way to constrain generic parameters; the Concept proposal has also been delayed for a long time [2]. In addition, C++'s Template capabilities are too powerful. If you are not developing libraries, I strongly recommend not using template metaprogramming; use only the generic-programming parts of templates. What should you do if you need some metaprogramming? Add a separate preprocessing stage to the build process and use another template engine to expand the source code. It is less error-prone, more readable, and produces friendlier error messages.
C++ has structured exception handling. I know some people would rather return error codes together with results than use the exception handling mechanism. But I only want to say that once you use exception handling, you will understand its value. In addition, exception handling has zero cost on the normal execution path [3].
Lambda expressions replaced function pointers. There is not much more to say about this; it was not easy to finally get this feature right after so many years.
Enum class also finally makes enum no longer just another way to define constants.
Object-Oriented Programming
Object-oriented programming (OOP) is generally not very useful and should not be used. However, generics in C++ are too cumbersome to use, and RTTI is too weak, so in most cases we use inheritance to implement polymorphism (subtype polymorphism) rather than generics.
Java, which claims to be a pure OOP language, is also used relatively rarely for polymorphism based on inheritance relationships. More often, people create an interface and then everyone interacts through that interface, or they constrain generics to an interface. In practice, Java interfaces play more of the role that Concepts [2] play in C++. Only when using a container to hold instances of these different types is a small amount of subtype polymorphism truly used. This scenario is not common. More often, after configuration is complete and the application starts, a container actually only needs to hold instances of a specific type. That said, there are still scenarios where a container holds instances of multiple types; they are just relatively rare, and most of them can be handled by having the container hold lambda functions. If that still cannot solve the problem, the only option is to create an interface type. After all, C++ does not have an Object type, and you cannot delete (void *)p.
OOP also creates quite a few problems. The more prominent ones were summarized in the GoF book Design Patterns, but if you do not use OOP, you do not have these problems [4].
However, OOP is still useful in some scenarios, such as UI and containers. But you have to be careful when using it. OOP always tempts you to apply OO where you should not. This comes from the cognitive conflict between the prior knowledge you naturally learned from the real world, which makes you think there should be an inheritance relationship, and the mathematical reality that no subtype relationship exists.
In short, it is still very good that C++ has OOP capabilities. You just have to be extremely careful when using them, and you also need to pay attention to the difference between value semantics and object semantics in C++ [5].
Other
There are also some concepts in C++ that are easy to confuse (I can only think of one for now):
constandconstexpr
const in C++ does too many things. For now, you only need to remember one principle: where Java would use final, C++ should use constexpr; use const only when creating a read-only version of an interface for an object.
Providing a dynamic-link library interface with C++ is very troublesome. My recommendation is not to use the pimpl pattern or approaches like Windows COM. Keep it simple: directly wrap a C version of the API.
Do not put large objects on the stack for no reason. Allocating an object with new does not have much overhead, and using unique_ptr can also solve the lifetime problem.