If I should summarize the main advantages of C++ in one short sentence I would say: C++ is a completely type safe and resource safe language without performance loss.
The type- and resource safety are two very powerful features if you use them right. Because of the very important principle “no performance loss”, C++ will often give responsibility to the developer. The language itself offers powerful and usually easy to use concepts which allows developers to implement in an efficient way. But on the other hand, the language will not add expensive management and validation layers to prevent programming errors because such concepts will compromise the performance. For example, C++ offers simple and lightweight pointer types. As a consequence, programming errors can lead to issues like dangling pointers and undefined behavior of the application. Other languages offer managed pointers to prevent such issues, but with the cost of performance loss.
Within this article I want to talk about the type-safety and resource-safety concepts and think about programming techniques we should use and programming errors we should avoid.
C++ has a static and strict type system. “Static” means that types are already known at compile time. Of course, C++ also offers techniques for late bindings during runtime. But these programming techniques will only extend the static type system by additional dynamic features. “Strict” on the other hand means, to check the type compatibility. For example, you cannot sum up an integer and a string variable or you cannot pass a const variable to a function expecting a non-const variable. Most of these compatibility checks can be done during compile time as the type system is static. Of course, if dynamic features are used the compatibility check is moved to runtime.
The type system of C++ allows definitions which go beyond the specification of the data type. Of course, you will define the data type, like “unsigned int”, “double”, “string” and so on but you can define additional type characteristics like constness and non-constness or for example whether the type is a pointer, reference or r-value. This allows to create use case specific data types which will limit they usage to the use case specific needs. For example, a function which needs to read a value could get this value as const parameter. This use case specific limitation will prevent wrong usage, for example you cannot by mistake set the parameter to a new value in this case.
The static and strict type system will allow to avoid programming errors without any performance loss. As mentioned before, all type system checks are done during compile time and therefore do not influence the runtime performance. C++ will not restrict the developer to use this static type system only. It is also possible to use dynamic types but with the downside that programming errors are not detectable during compile time. Therefore, related issues will occur during runtime. To prevent performance loss, there are typically not many type checks during runtime. Therefore, such programming errors will most often lead to undefined behavior.
As you want to use the powerful type system of C++ you should pay attention to the following guidelines. At first you should think about the type itself. For example: do you need an integer value or a long? At next, you have to define the constness of the type: Do you want to read or write to the parameter? And of course, you have to define whether you want to use the value directly or whether you want to use a pointer or reference. In summary: choose the right type according to the needs of the use case.
As mentioned before, C++ offers the flexibility to bypass this static and strict type system and use dynamic features. As this comes with some major disadvantages you should avoid such language features. Use such features only if there is no other possibility, for example in case you have to deal with third party components offering an interface which needs dynamic type system features. But aside from such special use cases, you should never bypass the strict type system. This means you must not use things like casts, void pointers or unions. The misuse of casts and unions can lead to type and memory violations. Therefore, you should avoid casts and use a variant class rather than a plain union in most cases.
Many articles about comparison of programming languages contain a topic about resource management. But most of them only compare the memory management. Therefore, you will find definitions like: C++ has an explicit memory management and C# has an implicit garbage collection. But unfortunately, such considerations limit the view on one special case and don’t explain the base resource management concepts of the languages. Of course, memory management is an important topic, but as we want to think about resource safety in general, we should keep in mind that there are several other resources. For example: files, streams, network connections, mutexes, database connections and many more.
C++’s model of resource management is based on the use of constructors and destructors. Constructors specify the meaning of object initialization and destructors define the object cleanup. For scoped objects, destruction is implicit executed at the end of the scope. For objects placed in the free store (heap, dynamic memory) using new, delete is required. This model has been part of C++ since the very earliest days.
This simple but efficient constructor/destructor mechanism was introduced to handle the resource management part. But there is one big issue: there are two ways to call the destructor. As mentioned before, one way is the automatic call at end of object scope. And the other way is the explicitly to implement call of “delete” for objects created by using “new”. This second possibility for object management is a big source of errors. Programming errors may lead to situation were “delete” is never called and therefore resources will not be cleaned up. Or it may lead to situation were a “delete” was executed and resources are already cleaned up, but the calling object nevertheless wants to work with these resources.
As conclusion, C++ offers a nice and lightweight resource management system, but we have to use it in the right way. A common technique is called RAII (resource acquisition is initialization). This programming technique says that all resources, needed by an object instance, must be initialized within the constructor and cleaned up within the destructor. C++ supports the RAII principle in a nearly perfect way, except the fact that it is possible that the destructor of an object instance is never called as that instance life cycle is not implemented correctly. For example, we can create object instances and never delete them.
Fortunately, there is a simple solution for this issue. As mentioned before there exists an automatic life time management for objects. Scoped object will be deleted automatic as the scope has left. So, let’s use this nice mechanism. That means: think about the “new” and “delete” functionality and the resource safe way to use this feature. According to the RAII principle, resources must be managed by objects. And of course, memory is an important resource. So, if you have to allocate and free memory by using “new” and “delete”, this resource must be managed within an object. That’s all! This simple concept avoids a lot of the typical implementation errors in C++. And of course, there are already build in features within the language which support this programming principle, for example smart pointers, lock classes and so on.
That means, don’t use wild “new” and “delete” commands somewhere in your implementation. Manage the needed memory within an object and use “new” in constructor and “delete” in destructor. Even if “new” and “delete” are used in a small scope, near to each other, they should not be used. For example, a gladly used implementation is to call “new” at the start of a method to initialize a resource and “delete” at the end of the method. It seems to be harmless if you do this wild memory management within this small scope of a method call. But each instruction within the method could result in an early end of the method, for example caused by an exception. In such situations, the “delete” is never called. Therefore, even in such supposed easy manageable situations, use the RAII principle and do the resource management by using an object. The standard “unique_ptr” class is most often a perfect solution for resource management within method scope.
C++ offers a very strong and strict type system and an easy to implement and performant resource safe system. If you consequently use these concepts you could write efficient code. Of course, source code will never be flawless but if you bypass these type safety and resource safety concepts your source code will become bad and error faulty very quickly.