A few years ago, Linus Torvalds criticized C++ and said:
inefficient abstracted programming models where two years down the road you notice that some abstraction wasn't very efficient, but now all your code depends on all the nice object models around it, and you cannot fix it without rewriting your app.
Many C++ developers do not appreciate this opinion. However, we must admit that every language feature comes at a cost, and it is better to understand that cost than to assume your favorite programming language is perfect.
Keep in mind that no language, technology, or operating system is perfect. Knowing its limitations and drawbacks, however, can help you use it more effectively.
For this purpose we will analyze the Git source code and discover some design facts. Git is a distributed revision control and source code management (SCM) system with an emphasis on speed. Git was initially designed and developed by Linus Torvalds for Linux kernel development; it has since been adopted by many other projects.
Let’s compare the refactoring overhead of some C++ OOP mechanisms with their C equivalents.
Modularity: Namespace vs Directory
Modularity is a software design technique that increases the extent to which software is composed of separate parts, making modular code easier to manage and maintain.
We can modularize a project with two approaches:
- Physically: by using directories and files. This form of modularity is provided by the operating system and can be applied to any language.
- Logically: by using namespaces, components, and classes. This technique depends on the capabilities of the language.
When developing in C, the code can be structured using directories to isolate modules. Here is the dependency graph between some of Git’s directories.

However, with C++ instead of C, we can use namespaces to modularize the code; these constructs are provided by the language. For the previous graph, we can use namespaces to modularize our code instead of directories.
Pros and cons of the C++ approach:Easy to understand: The logical approach is better because the modularity is well defined by the language artifacts, and just by reading the code we can know in which module a code element exists.
Managing changes: a good design generally needs many iterations, and with the physical approach the impact of design changes can be much more limited than with the logical one: indeed, we only need to move a function or variable from one file to another, or move a file from one directory to another. With C++, however, such a change can affect a significant amount of code because logical modularity is implemented through language constructs and therefore requires code modifications.
Encapsulation:Class vs File
In C++, encapsulation is the process of combining data and functions into a single unit called a class. Using the method of encapsulation, the programmer cannot directly access the data. Data is only accessible through the functions present inside the class.
In C, we can also achieve encapsulation, but through a physical approach like the one described in the modularity section: a class can be a file containing functions and the data used by them, and we can limit the accessibility of functions and variables by using the “static” keyword.
Git uses this technique to hide functions and variables; to discover that, let’s search for static functions:
from m in Methods where m.IsStatic select m
The treemap is very useful for getting a clear overview of the code elements affected by a CQLinq query; the blue rectangles represent the result.

Almost all functions are declared as static so that they are visible only within the translation unit in which they are declared; the same observation applies to variables.
from f in Fields where f.IsStatic select f
Easy to Understand:Using C++ encapsulation mechanisms improves code clarity and makes relationships easier to understand.
Managing changes: If we have to change the place where a variable or function is encapsulated, it can be very easy for C, but for C++ it can impact a lot of code.
Polymorphism vs Selection idiom
Polymorphism means that code, operations, or objects can behave differently in different contexts.
This technique is widely used in C++ projects, but what about C?
In procedural languages, selection is typically implemented using keywords such as “switch”, “if”, or even “goto”, but this approach tends to increase the cyclomatic complexity of the code.
Let’s search for complex functions inside the Git source code.

Even though Git is well developed, many functions could be considered complex. This is partly due to the extensive use of control-flow statements such as “if”, “switch”, and “goto”. With C++, however, we can use polymorphism to minimize the complexity of the code.
Easy to understand: Using polymorphism allows the isolation of a specific behavior in a class; it improves the visibility and the cohesion of the code.
Managing changes: Adding another behavior with polymorphism can imply adding another class; however, with the selection idiom, you only need to add another case under the switch statement.
Inheritance vs Composition
Git uses essentially structs to define data manipulated by functions. Let’s search for all structs used:
from t in Types where t.IsStructure select t

Interestingly, almost all data is encapsulated within structs. To verify this, we can search for all non-const public primitive variables that are not inside a struct:
from f in Fields where f.IsPublic && f.IsPrimitiveType
&& !f.IsStatic && !f.IsConst
select f

Only a few variables are concerned, which is a good point for the Git design.
So what about extending a struct? With C we can use composition, as in the case of the “remote” struct, which many structs reference.

However, with C++ we can also use inheritance to extend structs; for example, the known_remote struct could inherit from the remote one.
Easy to understand: using inheritance can improve the understanding of data, but we have to be careful when using it; it’s used only for the “Is-a” relation.
Managing changes: Inheritance implies high coupling, so any change can impact a lot of code.
Conclusion:
C++ provides more ways to create clean, well-structured code, but this comes at a cost: changes and refactoring can be more difficult.
Refactoring requires understanding the existing code before making changes. C programs can be more difficult to understand, but easier to modify.
How can we limit the impact of changes in C++?
A good way to limit the impact of changes is to use design patterns, particularly the principles of loose coupling and high cohesion, to isolate changes in specific areas. Irrlicht, as explained in the previous post, is a good example of using loose coupling.
