Linux, PHP, and Git are popular projects developed in C. On the other hand, OpenOffice, Firefox, Clang, and Photoshop are developed in C++, showing that both languages are well suited to developing complex applications. Trying to prove that one language is better than the other may not be the most useful debate. However, we can discuss the motivations behind choosing one over the other.
When I first read the opinion of Linus Torvalds on C++, I completely disagreed with his point of view as a C++ developer. But this is the perspective of the lead developer of the Linux kernel and Git, so it deserves careful consideration.
After rereading his opinion, I found myself agreeing with this assertion:
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.It’s true that C++ offers more possibilities for writing elegant, well-structured code, but this comes at a price: changes and refactoring can be difficult. However, that doesn’t mean I have to choose another language. Every language or library involves trade-offs, and we need to know how to limit the impact of potential changes to our C++ code after years of development.
Let’s analyze the Git source code with CppDepend, identify some design characteristics, and compare C and C++ in two areas:
- Easy to understand.
- Managing changes.
Modularity: Physical vs Logical
Modularity is a software design technique that increases the extent to which software is composed of separate parts; modular code is easier to manage and maintain.
We can modularize a project using 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, classes, and structs. This technique depends on the language’s capabilities.
When developing in C, we mainly use physical modularity to organize our code. Directories are used to isolate modules. Here’s the dependency graph between some of Git’s directories:

However, with C++ we can use namespaces to modularize the code base; these artifacts are provided by the language itself. In the previous graph, the shapes could be namespaces modularizing our code instead of directories.
The impact of choosing one of these two approaches:Easy to understand: the logical approach is better because modularity is clearly defined by language constructs, and simply by reading the code, we can tell which module a code element belongs to.
Managing changes: good design generally requires many iterations, and with the physical approach, the impact of design changes can be much more limited than with the logical one. We may only need to move a function or variable from one file to another, or move a file from one directory to another.
In C++, however, such a change can affect a lot of code because logical modularity is implemented through language constructs and therefore requires code changes.
Encapsulation: Class vs File
In C++, encapsulation is defined as the process of combining data and functions into a single unit called a class. With encapsulation, the programmer cannot access the data directly; 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 file can contain functions and the data they use, and we can restrict the visibility of functions and variables with the “static” keyword.
Git uses this technique to hide functions and variables. To verify this, let’s search for static functions:
from m in Methods where m.IsStatic select m
The treemap is very useful for getting an overview of the code elements matched by a CQLinq query; the blue rectangles represent the results.

Almost all functions are declared static so that they are visible only within the translation unit in which they are declared; the same applies to variables.
from f in Fields where f.IsStatic select f
Easy to Understand:Using C++ encapsulation mechanisms improves code readability and organization; C is lower-level and relies more on a physical approach than a logical one.
Managing changes: if we need to change where a variable or function is encapsulated, it can be very easy in C, whereas in C++ it can affect a lot of code.
Polymorphism vs the Selection idiom
Polymorphism means that code, operations or objects behave differently in different contexts.
This technique is widely used in C++ projects, but what about C?
In procedural languages, selection is implemented through keywords such as “switch”, “if”, or even “goto”, but this technique 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 designed, many functions could be considered complex. This is partly due to the extensive use of control-flow statements such as “if”, “switch”, and “goto”. In C++, however, we can use polymorphism to reduce code complexity.
Easy to understand: Using polymorphism allows specific behavior to be isolated within a class, improving code readability and cohesion.
Managing changes: Adding another behavior with polymorphism may require adding another class; with the selection idiom, however, you only need to add another case to the switch statement.
Inheritance vs Composition
Git primarily uses structs to define the data manipulated by functions. Let’s search for all the structs it uses:
from t in Types where t.IsStructure select t

Interestingly, almost all data is contained 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 match this query, which is a positive aspect of Git’s design.
So what about extending a struct? In C we can use composition, as with the “remote” struct, which many structs reference.

In C++, however, we can also use inheritance to extend structs; for example, the known_remote struct could inherit from remote.
Easy to understand: using inheritance can make data relationships easier to understand, but it must be used carefully; it should only be used for an “is-a” relationship.
Managing changes: inheritance introduces tighter coupling, so a change can affect a lot of code.
Conclusion:
C++ offers more possibilities for writing elegant, well-structured code, but this comes at a price: changes and refactoring can be difficult.
Refactoring requires understanding the existing code before making changes. C programs can be more difficult to understand but easier to modify, while a C++ project can be more structured than a C project but may require more effort when changes are made.
How can we limit the impact of changes in C++?
A good way to limit the impact of changes when using an OOP approach is to apply design patterns, particularly the principles of loose coupling and high cohesion, to isolate changes in specific areas.
Another effective approach is to adopt generic programming and modern C++ practices. Generic programming can be more flexible than OOP and can help limit the impact of changes to C++ code.
