Coding a project in C, C++ and Python and Comparing them


 

I completed the same project in three different programming languages: Python, C++, and C. Why? Because I was curious to explore the differences between them—their advantages, disadvantages, and unique features.

At first glance, we can briefly summarize what each language offers and even mention its drawbacks.

For example, we know that Python offers simplicity and ease of use. You don’t have to manage memory yourself, and since it’s an interpreted language, you also get some level of metaprogramming and reflection capabilities. Additionally, many things have already been implemented for you, so you get a lot of help.

The problem with Python is its performance—it is known to be a very slow programming language, at least compared to C or C++. But my question is: does Python’s convenience pay off despite its poor performance? Or at the very least, can we have some metrics to understand just how much slower it is compared to other languages?

With C++, on the other hand, the first benefit we notice is that it’s significantly faster. It’s a compiled language that leverages excellent backends like GCC and LLVM. The instructions are executed directly by the CPU, and compiler backends have been optimized over the years to achieve performance that almost grazes perfection. And that’s great. The problem? It’s much more complex—mainly because you have to manage memory yourself, which adds extra work.

But there’s another competitor in the scene: C. Why choose C over C++? This is a heavily debated topic on the internet. Many people prefer C over C++ precisely because of its complexity. C++ is known as an extremely complex language—it introduces templates, constructors, destructors, overloading, polymorphism, and a bunch of other stuff, while C is pretty straightforward.

The worst part of C++ is that it’s an old language that has been evolving over time, constantly introducing new concepts and adapting to modern technologies while maintaining compatibility with older C++ versions. See the problem? This evolution—without the freedom to break compatibility—has led to ugly syntax, bad implementations, and even broken design choices.

C, on the other hand, was born simple—as an abstraction over assembly—and that’s exactly what it remains today. Yes, there have been some changes and improvements, but they are minimal, keeping its core intact: simple and minimal.

So, I want to compare them—to see if all the complexity, noise and shit in C++ are justified when considering the new concepts and conveniences it provides, or if, in the end, it just falls behind the simplicity and minimalism of C.

This is the premise of this project.

What is the project about?

To test these programming languages, we need a project that is simple enough for me to implement in three different languages, yet complex enough to allow for meaningful performance measurements.

So, I decided to implement an image generator that exports directly to a bitmap. The project consists of two parts: generating the image as an array of bytes and exporting that array to disk using the Bitmap format.

All of this is done without any external dependencies—only the standard library that each language provides.

Generating the image

The first thing we need is an array to store all the pixels of the image. The image will be in grayscale, so we only need one byte per pixel to represent its brightness level.

|0|0|0|0|0|0|0|0|
|0|0|0|0|0|0|0|0|
|0|0|0|0|0|0|0|0|
|0|0|0|0|0|0|0|0|
|0|0|0|0|0|0|0|0|
|0|0|0|0|0|0|0|0|
|0|0|0|0|0|0|0|0|

All pixels will start with a value of 0, meaning they are completely dark. Then, we will read a file from disk containing a list of instructions to apply to our image. These instructions look like this:

toggle 461,550 through 564,900
turn off 370,39 through 425,839
turn off 464,858 through 833,915
turn off 812,389 through 865,874
turn on 599,989 through 806,993
turn on 376,415 through 768,548
turn on 606,361 through 892,600
turn off 448,208 through 645,684
toggle 50,472 through 452,788
toggle 205,417 through 703,826
...

each line, represents an operation that we have to apply to our image. then each line have two components:

    op             region
| toggle | 461,550 through 564,900 |

the first indicate the operation. it could be:

  • toggle: we need to sum up the pixel’s current value by 2.
  • turn on: we need to sum up the pixel’s current value by 1.
  • turn off: we need to substracts the pixel’s current value by 1 just upto 0, then it doesn’t have any effects.

the next part, indicates a rectangular section where we have to execute the operation: it is conformed by two points indicating the up left corner and down rigth corner of the rectangle, and the through keyword that can be ignored.

So, the first thing we need to implement is a parser that transform those meaningless bunch of text into something that a computer can understand.

  • in python:
class Range:
    def __init__(self, p1, p2):
        self.start = (int(p1[0]), int(p1[1]))
        self.end = (int(p2[0]), int(p2[1]))

class Operation(Enum):
    TURN_ON = auto()
    TURN_OFF = auto()
    TOGGLE = auto()

class Instruction:
    def __init__(self, op, range_):
        self.op = op
        self.range = range_
  • C++:
struct Point2d{
    size_t x, y;

    Point2d(size_t x=0, size_t y=0):
        x{x},
        y{y}
    {}
};


struct Range{
    Point2d start, end;

    Range(Point2d s, Point2d e):
        start{s},
        end{e}
    {}
};

enum class Operation{
    TURN_OFF,
    TURN_ON,
    TOGGLE
};


class Instruction{
private:
    Range m_range;
    Operation m_op;

public:
    Instruction(Operation op, Range range):
        m_range{range},
        m_op{op}
    {}
}
  • C:
enum Operation{
    OP_TURN_OFF,
    OP_TURN_ON,
    OP_TOGGLE
};

struct Vect2d{
    int32_t x, y;
};

struct Range{
    struct Vect2d start, end;
};


struct Instruction{
    struct Range range;
    enum Operation op;
};

the parsing is very simple, we are iterating over the file line by line and extracting elements as we go.

"|toggle| 4,4 through 5,5"
    op

We extract first the op from the line and returns the next part:

|4,4| through 5,5
  p1

then we extract the first point and returns what remains.

|through| 5,5
 ignored

we discards the through keyword, and returns the last part, that is the last point.

5,5
 p2

So, finally we only need to parse that final point, we gather all these data into the Instruction class to be able to work wit it later.

the python version was the easier one, as expected. I implement this logic into the from_line static method of the Instruction class like this:

class Instruction:
    def __init__(self, op, range_):
        self.op = op
        self.range = range_

    @staticmethod
    def _extract_operation_from_line(line):
        if line.startswith('toggle'):
            return Operation.TOGGLE, line.removeprefix('toggle ')
        elif line.startswith('turn on'):
            return Operation.TURN_ON, line.removeprefix('turn on ')
        elif line.startswith('turn off'):
            return Operation.TURN_OFF, line.removeprefix('turn off ')
        else:
            raise Exception('WTF DUDE')

    @staticmethod
    def _extract_range_from_line(line):
        lps = line.split(' ')
        p1 = lps[0].split(',')
        p2 = lps[2].split(',')
        return Range(p1, p2)


    @staticmethod
    def from_line(line):
        op, line = Instruction._extract_operation_from_line(line) 
        range_ = Instruction._extract_range_from_line(line)
        return Instruction(op, range_)

As you can see, I take some shortcuts in the Python version, but in the C and C++ versions, the logic is fully implemented and more verbose.

Applying instrcutions to the grid

Now, that we already have the instructions line parsed into something that computer can handle easier. Let’s apply it to our image.

  • Python version:
class Instruction:
    ...

    def apply_to(self, grid):
        start = self.range.start
        end = self.range.end
        for rowidx in range(start[1], end[1] + 1):
            for columnidx in range(start[0], end[0] + 1):
                actualidx = (rowidx * IMG_WIDTH) + columnidx
                grid[actualidx] = self._get_new_value_for_cell(grid[actualidx])
  • C++ version:
class Instruction:
    ...

    void apply_to(std::vector<size_t>& grid) const{
        size_t row_start = m_range.start.y;
        size_t row_end = m_range.end.y;
        size_t column_start = m_range.start.x;
        size_t column_end = m_range.end.x;

        for (size_t i=row_start; i<=row_end; ++i){
            for (size_t j=column_start; j<=column_end; ++j){
                size_t actualidx = (i*IMG_WIDTH) + j;
                grid[actualidx] = process_cell_value(grid[actualidx]);
            }
        }
    }
}
  • C version:
void apply_instruction_to_grid(struct Instruction inst, size_t grid[IMG_HEIGHT * IMG_WIDTH]){
    size_t row_start = inst.range.start.y;
    size_t row_end = inst.range.end.y;
    size_t column_start = inst.range.start.x;
    size_t column_end = inst.range.end.x;

    for (size_t i=row_start; i<=row_end; ++i){
        for (size_t j=column_start; j<=column_end; ++j){
            size_t actualidx = (i*IMG_WIDTH) + j;
            grid[actualidx] = process_new_cell_value(inst.op, grid[actualidx]);
        }
    }
}

As you can see, we only need to iterate over the rows and columns specified by the instruction region and compute their new values based on the operation.

Once we finished the first part of the project, I decided to test it before continuing to the next part. It is better to catch errors and mistakes as soon as possible.

So, I implemented a pretty print for my image in memory and faked some instructions.

turn on 0,0 through 9,9
turn off 4,0 through 5,9
turn off 0,4 through 9,5
toggle 4,4 through 5,5

I executed it, and this is the output I got:

1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
0,0,0,0,2,2,0,0,0,0,
0,0,0,0,2,2,0,0,0,0,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,

A beautiful flag.

Bitmap generation

Now that I have a raw image generated in memory, we need a way to save it to disk.

Before starting, I have to admit that I had two major misconceptions:

  1. I thought generating a bitmap image was as simple as dumping the raw bytes from memory to disk.
    • No, it isn’t. While it’s easy to implement, it has its own tricks.
  2. I believed writing this image would be easier in C or C++ than in Python due to their low-level nature. I assumed I could just dump arrays and structures from memory to disk using memcpy or something similar.
    • No, it wasn’t. The culprit? Memory alignment. I ended up using the same approach as in Python—writing all bytes manually.

So, What’s next?

I already have some kind of image in memory, but it’s a bit odd. It only stores a single value—lightness—whereas an actual image consists of three values: RGB.

Additionally, I want to interpolate the pixel values so that:

  • The minimum value in the image appears completely dark.
  • The maximum value appears completely bright.

In color theory:

  • Completely dark is 0.
  • Completely white is 255.

However, my image values might range from 5 to 20 or 0 to 1—it varies.

So, let’s first create a function that takes our non-normalized image and returns a normalized one.

NOTE: Since the code is very similar across languages, I’ll only show one version at a time.

def interpolate_color(x1, x2, x):
    y1 = 0
    y2 = 255
    if x1 == x2:
        raise ValueError("x1 and x2 cannot be the same")

    return math.floor(y1 + (x - x1) * ((y2 - y1) / (x2 - x1)))


def to_img(grid):
    max_val = max(grid)
    min_val = min(grid)
    ngrid = list(map(lambda c: tuple([interpolate_color(min_val, max_val, c)] * 3), grid))

here, python is a little bit easier because all the modern features, in C/C++ on the other hand I did the same but lazyly so I read each pixel, translate it and write directly to file, the part that normalize the pixels is this:

/* C version */
size_t interpolate_pixel(size_t rp, size_t min, size_t max) {
    const size_t miny = 0;
    const size_t maxy = 255;
    if (min == max) return 0;
    return miny + (rp - min) * ((maxy - miny) / (max - min));
}

this was done that way, because I first code the project in python and then in c and c++, so I get more ideas about it. but do not worry about measurement, we are going to optimize the python version later.

then that we normalized the image, we have to convert it to an actual RGB image, in python that is done actually here:

ngrid = list(map(lambda c: tuple([interpolate_color(min_val, max_val, c)] * 3), grid))
                                 --------------------------------------------
                                                                          ^
                                                                          |
                                                        look how I multiply the array by 3 to get RGB

in C++ I create an actual RGB struct, that convert one value to 3 in gray scale:

struct RGB{
    uint8_t r{0},g{0},b{0};

    RGB() = default;

    RGB(uint8_t val):
        r{val},
        g{val},
        b{val}
    {}
};

In C, it’s the same, just with C-style syntax—you know how it is.

Now, with the actual image ready, we just need to save it to disk using the bitmap format.

To be honest, this involves too many bytes for me to explain in detail, so I’ll just give you a high-level overview of the format. You can check the code later.

Bitmap format

Ok, so let me introduce you to the bitmap format. Here, we are writing raw bytes to a file.

The bitmap format is divided into four main sections:

Bitmap header

These are the first 14 bytes in the file. In this section, we specify general information about the image. It is structured as follows:

  • First 2 bytes: These are just the ASCII letters “BM”, which serve as a signature. This allows tools to instantly identify the file format without relying on the extension.
  • Next 4 bytes: An unsigned integer representing the total size of the file, including this header, in bytes.
  • Next 4 bytes: Reserved for whatever you want. This is useful if tools like GIMP or Aseprite want to store metadata in the file. In my case, I simply set it to 0.
  • Next 4 bytes: The byte offset indicating where the image data starts. This is important because some headers have variable sizes.

And that’s all.

DIB Header

After the general header, we need to add a DIB header (Device-Independent Bitmap header). This section holds specific information about the image data. Interestingly, there isn’t just one standard for this header—there are more than six or eight different versions.

Each version adds features to the bitmap format. For example:

  • BITMAPV5HEADER: Implemented by GIMP, this version introduces many features like compression, transparency, and other enhancements. I’m not a bitmap expert, so I won’t list them all.

  • BITMAPV3INFOHEADER: Introduced by Adobe Photoshop, this version wasn’t officially documented until an Adobe employee published an unofficial specification. You know how these companies are… F**k you, Adobe. I mean, I completely understand that as a business, you want profitability (I do too), and you want to protect the software and code you invested money in developing. But there are limits, bro. A bitmap implementation!? Really?

  • BITMAPINFOHEADER: Implemented by Microsoft for Windows 3.1, and properly documented by Microsoft. Can you see that, Adobe?

  • BITMAPCOREHEADER: Also developed by Microsoft, but for Windows 2.0. This is the oldest version, the first implementation of the bitmap format. It has no extra features at all—which, for me, is a good thing. Fewer features, less work. So this is the one I chose for this project.

this header consists only of 12 bytes:

  • First 4 bytes: An unsigned integer indicating the size of this header (12 bytes).
  • Next 2 bytes: The width of the image.
  • Next 2 bytes: The height of the image.
  • Next 2 bytes: The number of color planes. I have no idea what this does, but Wikipedia says it must always be 1, so…
  • Next 2 bytes: The number of bits per pixel. In my case, it’s 8 because I don’t want to deal with bitwise operations.

But what does bits per pixel actually mean? I’ll explain that in the next section.

With these 12 bytes, we complete our DIB header.

Color Palette

What follows now is the color palette.

As you should know, an image is just a collection of pixels, and each pixel contains 3 bytes: Red, Green, and Blue (RGB). The combination of these three bytes forms all possible colors.

But here’s the thing—the same color can appear multiple times in an image. For example, let’s take the flag from earlier…

1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
0,0,0,0,2,2,0,0,0,0,
0,0,0,0,2,2,0,0,0,0,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,
1,1,1,1,0,0,1,1,1,1,

Let’s suppose that 1 = red, 0 = white, and 2 = blue.

As you can see, this is a 10×10 pixel image, meaning it contains 100 pixels in total. Since each pixel is 3 bytes long, the total image data would be 100 × 3 = 300 bytes.

However, in software development, whenever patterns and redundancy exist, a more optimal solution can often be found. In this case, the redundancy is in the repeated colors.

So, what can we do?

This is where the color table comes into play. Instead of writing the actual RGB bytes directly into the image data, we store them separately in a table. This way, each color is written only once, eliminating repetition.

#COLOR TABLE
[RED, WHITE, BLUE]
  0     1      2

Now, in our color table, the first color is red, the second is white, and the third is blue.

Instead of saving the actual 3-byte RGB color for each pixel in the image data, we can store only the index that points to the corresponding color in the table. This way, each pixel requires only 1 byte instead of 3 bytes. So, instead of 100 × 3 = 300 bytes, the image will only take 100 × 1 = 100 bytes, reducing its size by more than half.

Of course, there are cases where this approach might be less efficient—for example, an image with a lot of colors. But in general, this is the most optimal way to store an image without compression.

Now, do you remember the bits per pixel (bpp) field we mentioned earlier? It determines how many colors our color table can store and how many bits (not bytes) each pixel requires.

For example:

  • 1 bit per pixel → 2 colors
  • 2 bits per pixel → 4 colors
  • 3 bits per pixel → 8 colors
  • 4 bits per pixel → 16 colors
  • 5 bits per pixel → 32 colors
  • 6 bits per pixel → 64 colors
  • 7 bits per pixel → 128 colors
  • 8 bits per pixel → 256 colors

As you can see, the formula to calculate the maximum number of colors is 2^bpp.

In our case, we are using 8 bits per pixel, meaning we can support 256 colors, even though we only need 3 colors. The remaining unused colors still need to be defined—typically set to white or some placeholder value.

This means we’re wasting some space, but I made this decision to keep the project simple. The smallest unit that modern computers efficiently handle is 1 byte (8 bits), and I didn’t want to deal with bit-packing complexities.

Image data

After writing our color palette with all colors that our image need, is time to write our actual image to the file.

first we need to translate the color to the corresponding index in the color palette for each pixel introduced. but that is not all, our life cannot be as easy. We need to do it backwards. and if you think that this is not that hard, you only have to reverse the rows not the columns. like this:

1,2,3,4
5,6,7,8
9,0,1,2

---

9,0,1,2
5,6,7,8
1,2,3,4

why is this, I don’t fucking know, but well it is what it is.

but we are no finish yet. each row need to have at least a multiple of 4 bytes, in our case, our image is a 10x10 pixel, so our row size are 10 pixels long, and each pixel is 1 byte, so each row takes just 10 pixel and we need at least 2 more as padding:

the formula to calculate the size for our rows is this:

ceil((bpp*width)/32) * 4

and now with this we are finish.

we can execute our program and see our image.

Not that fast

Well, in fact, it didn’t work at first, we aren’t perfect. So I had to delve into the more obscure thing, Debug a binary file. I had to open the image in my hexadecimal editor, and check and count byte per byte. but everything for science, after some time counting raw bytes, we get it. An image generated by ourselves in three different languages Python, C++ and C.

you have to make a little bit of zoom, is an 10x10 pixel image but, look at it.

Now, let’s execute our 300 lines of instructions in our input file. it obviously took more time but the result was this:



Some benchmarking

Since we already have our image generator done, let’s measure its performance. I ran each version, and these were the results:



 

I passed 300 instructions to execute and:

  • The python version took: almost 6 seconds to complete.
  • The C++ Version Took: 360 milliseconds.
  • And the C version Took: 180 milliseconds.

As you can see, the C++ version tooks literally 2 times more to complete than the C version, and Im suspecting that the culprit wasn’t C++ if not me. I’m not a C++ expert. But anyways, what really calls the attention is the python version. 6 seconds!!! it takes 16 times more than C and C++!

I was curious what would Happen if I compile the C and C++ versions with Compilers Optimizations on:

  • C++ Optimized tooks: 60ms
  • C Optimized Tooks: 40ms

literally we can run our C++ version 100 times while python complete. this discrepancy led me to think that there was something pretty bad in my python code. so I start to refactoring it.

Refactoring python code

First of all, what is the slowest part in my python source code? Let’s run the python profiler cProfile to see it. the output is this.


 

As you can see, one of the culprit of the bad performance is the apply_to function. the profiled version took 14 seconds in complete and this function took 11 seconds.

the profilers is showing us that this function is called 300 times and the main overhead of it is the call to _get_new_value_for_cell.

def _get_new_value_for_cell(self, old_value):
    if self.op == Operation.TURN_ON:
        return old_value + 1
    elif self.op == Operation.TURN_OFF:
        return max(0, old_value - 1)
    elif self.op == Operation.TOGGLE:
        return old_value + 2
    else:
        raise Exception("WTF BROH")

def apply_to(self, grid):
    start = self.range.start
    end = self.range.end
    for rowidx in range(start[1], end[1] + 1):
        for columnidx in range(start[0], end[0] + 1):
            actualidx = (rowidx * IMG_WIDTH) + columnidx
            grid[actualidx] = self._get_new_value_for_cell(grid[actualidx])

Looking at those functions, they are pretty compact and straightforward. Yes, we have two loops here, but if you check carefully, you’ll see they are completely necessary. I mean, I could try to remove them by using an actual two-dimensional array, but we’d still end up in the same situation—we have to iterate over a specific range of rows and a specific range of columns.

Another thing I could do to squeeze out some performance is manually inlining _get_new_value_for_cell. These are micro-optimizations, of course, but what else can we do? So:

def apply_to(self, grid):
    start = self.range.start
    end = self.range.end
    for rowidx in range(start[1], end[1] + 1):
        for columnidx in range(start[0], end[0] + 1):
            actualidx = (rowidx * IMG_WIDTH) + columnidx

            op = self.op
            if op == Operation.TURN_ON:
                grid[actualidx] += 1
            elif op == Operation.TURN_OFF:
                grid[actualidx] += -1 if grid[actualidx] >= 1 else 0
            elif self.op == Operation.TOGGLE:
                grid[actualidx] += 2
            else:
                raise Exception("WTF BROH")

Here, I’m manually inlining the function into one. That way, I avoid the overhead of calling a function, retrieving the pixel, passing it to the function, receiving the returned new value, and then inserting it back into the grid.

Let’s measure again to see how much this optimization improves that bottleneck:


 

Well, the time was reduced from 14 seconds to only 9 seconds, and now the apply_to function is taking just 5 seconds. So yes, this micro-optimization did help.

But what else can I optimize? The apply_to function is still the major bottleneck, but it’s both simple and necessary—I need to calculate the new value for each pixel in the range. I could process it lazily, like in C, but that wouldn’t improve performance, only reduce memory usage.

I also tried optimizing the create_bmp function, but I ran into the same issue—I had almost nothing to optimize. I tested changing list declarations, like using [0] * N instead of appending elements dynamically, to see if reallocation was the issue. But it wasn’t.

The real problem is the loops. There are too many, and Python is just slow at executing them. The slowness accumulates with each iteration.

So, does that mean the code is unoptimizable? Not necessarily. We could try multithreading or merging overlapping operations to reduce looping, but that’s a bit much for me right now. And one thing I’m sure of—it’ll never outperform the C or C++ version, even in debug mode. You can try if you’re brave enough.

After my miserable attempt at optimization, I got 5.65 seconds. What a failure. But then I ran the same Python code with PyPy… and it executed in just 1.57 seconds. Incredible. Maybe I’m not that bad at programming after all—this just proves that the real culprit wasn’t me.

Memory

Now this is the memory consumption for each language:


 

Here python is using 98.5 MB of memory while C and C++ are using around 10mb only, That was expectable because first we are lazily writing the image ot the file in the C and C++ version, and second because it is python.

The refactored version of python is the same, but was is unusual at least for me is pypy that is using 262 MB of memory. I do not know why, but it seems that is common in pypy.

Conclusions

I know these benchmarks weren’t that technical, but I hope they serve as a reference point for someone.

Now, let’s talk a bit about the languages themselves—the syntax, features, what I’ve noticed, and what I think is worth using depending on the situation.

Python doesn’t like loops

First assertion: Python is a phenomenal language. I use it every day, and I like it. Yes, it’s slow, but let’s be honest—a difference of milliseconds doesn’t matter in most cases; it’s imperceptible.

So, it’s a blessing to have a language that manages memory by itself, has everything I need in the standard library by default, and benefits from a huge community that expands it even further.

The syntax? Well, for me (a curly brace lover), it’s not the best, but at the end of the day, programming is programming. If, while, and for statements are conceptually the same everywhere.

The problem arises when you need real-time performance and have to loop over data repeatedly. Those milliseconds that didn’t matter before start adding up, eventually forming an army that will destroy your app’s performance.

So yes—Python is not ideal for real-time, loop-intensive applications.

C and C++

Now, let’s get to the real battle: C or C++?

The core issue here is simplicity. Many argue that C++ is overly complicated and prefer the simplicity of C.

To analyze this, let’s take a look at what C++ adds over C, particularly in the areas where this complexity debate seems to stem from.

Operator overloading

Let’s start simple.

In C++, you can define any operator for any struct (+, -, [], etc.).

C lovers see this as unnecessary indirection. They like to see each line of code in every part of the file and be able to predict the generated assembly for that line.

I do not agree with them on this point. Yes, conceptually, it does add indirection because you are calling a function that is not explicitly written in the same place. However, this can be resolved with some simple logic. If you see two non-fundamental types, like two 2D vectors, you should immediately recognize that an operator is involved.

This is a minor issue. I personally like operator overloading—it’s a pretty useful feature when used appropriately. However, if it were removed, it wouldn’t be the end of the world for me.

//C++
Vector2d p1(10, 20);
Vector2d p2(5, 15);
Vector2d result = p1 + p2;

//C
struct Vector2d p1 = {10, 20};
struct Vector2d p2 = {5, 15};
struct Vector2d result = Vector_add(p1, p2);

Indirection here is not a problem, the problem could comes later with Virtual functions and Exceptions.

Templates

Templates are pretty good. Yes, they add some complexity—at least theoretically—but at the end of the day, they simplify your codebase by allowing you to reuse code with different types.

They do not introduce any indirection. Templates are just code generation built directly into the language—nothing more. I’m not talking about template metaprogramming—that’s a whole other beast you only need to understand if you’re willing to deal with it. But regular templates are purely beneficial.

Without templates, we are left with these options:

  • Rewriting our functions or structs for each type we need, leading to a lot of code duplication.
  • Using void* to accept a pointer of any type. This limits us to working with pointers instead of values and makes the code more complex, not to mention the lack of type safety—since you’re casting from any type to any other type.
  • Relying on macros to achieve something similar to code generation. This essentially replicates what templates offer in C++, but in a much uglier and less flexible way.
  • Generating code with an external tool like Python.

See the problem? Templates are simply a good feature to have in the language. The supposed complexity they introduce is a myth—they actually make coding easier.

OOP

OOP is not bad at all; in fact, it’s just a way to organize code. You can think in terms of objects in C too. The problem arises with constructors and destructors, at least in the way they’re implemented in C++.

Constructors and destructors work well in managed languages, but in languages where you have total control over memory, they can get a bit complicated, especially with how they’re handled in C++.

The issue is that new doesn’t just do one thing—it does two: it allocates memory and initializes it, which introduces some subtle nuances. Similarly, destructors add some indirection that could easily be managed with the defer keyword, which many modern languages have implemented.

The way OOP was introduced in C++ with constructors also added a lot of complexity to the language. Now we need to deal with std::move, std::forward, LValues, RValues, and define a plethora of constructors and destructors while adhering to protocols like the Rule of Three or Rule of Five.

They later introduced smart pointers to solve the majority of these problems and provide more safety while coding. However, I’d argue that this is still the ugliest implementation among all languages.

Rust handles the same issue in an overkill manner, adding a borrow checker and limiting how you code. On the other hand, Zig and C3 aim to solve this problem simply, using defer and factory functions, much like C.

Also, polymorphism in C++ is done in one of the ugliest ways possible. There are no interfaces—just inheritance from a pure virtual class.

That being said, I think C++ still brings a lot of benefits and can be very helpful compared to plain C.

Yes, absolutely everything can be done in C, but it’s much more cumbersome. If you don’t like certain features in C++, simply don’t use them.

To much can be bad

So, is C++ a better language than C? For me, yes, it is. There are situations where C is better, of course, and times when I wish I were coding in C. But I think C++ offers more benefits than drawbacks.

One of the reasons why many standard projects, like Linux and CPython, choose C over C++ is because C++ offers too much, while C doesn’t. This may sound strange, but it’s true.

In C, there’s usually only one way to solve a problem, and most developers will agree on it. In C++, however, there are many ways to solve a problem, and more than one of them can be correct. It’s often a matter of personal choice.

This is why C development is a happy world: there’s usually a clear path to take. In C++, though, we often spend a month discussing how we’re going to solve a problem instead of actually solving it.

C++ is an evolving language. It’s been adapting its entire life, implementing new concepts that are well-accepted and others that aren’t as good. This makes the language seem “dirty,” and indeed, it can be messy. But it’s also very useful, and if you learn to recognize the trash among the features, you get a very powerful language.

Don’t let Linus’ statements about C++ freak you out. I think he was right when he made those comments, considering that C++ was originally just a transpiler to C and didn’t have many of the features it has today. But for me, it’s the way to go now. If you want portability, use extern "C". If you’re working in an environment with a capable C++ compiler, then go with C++.

The only group I’m not sure about when it comes to C++ is compiler implementers, because while we can ignore the mess, they can’t.

Conclusion

C++ is like working with top-tier computers, cutting-edge technology, and AI, but with very noisy neighbors and music blasting at full volume. On the other hand, C is like working in a nice, clean, and comfortable environment, but with a Commodore that has no internet connection.

My question is: would other languages fit in the middle, being the perfect one? Rust, Zig, C3, Go? I don’t know, and that’s not the topic of this video.

Thanks and Good bye

If you’re here, I have to thank you for reading. I really hope this article (or video, if you’re on YouTube) has helped you.

* The source code of this project: https://drive.google.com/file/d/1CJTcan9Q4DoIrF51f5ThT7hxpEMtbvyx/view?usp=drive_link

* My channel: https://www.youtube.com/@Jorvix11 

* My X: https://x.com/soworksl

* My LinkedIn: https://www.linkedin.com/in/jimy-aguasviva-781b32200/

Comments

Popular posts from this blog

Interpolacion Lineal, un gran aliado para aplicaciones responsive y dinamicas

Microsoft Word vs Latex, Una comparacion a detalle.