Thursday, December 14, 2023

The Essence of "Modern C++"

I can remember my first teenage attempts at learning programming in an imperative OO style with C++ in the late 1990s. That was the pre-stackoverflow, pre-Web 2.0 era when the web generally consisted of a few keyword-based search engines and millions of weird personal pages with those nasty "blink" tags. Any educational material covering my interest in programming was sparse and hard to come by. Living in a poor Eastern European post-communist country that was still pretty much isolated from the Western world and not knowing any conversational English didn't help, either. My best bet were a couple of locally-available, badly-translated C++ books that were mediocre to begin with. Having access to anything from Scott Meyers in English was probably a privilege for the very few.

The common ethos of those books revolved around inheritance, virtual functions and operator overloading. They never showed you how to approach a real programming problem. I was eager to learn how to create a basic game with moving stuff on the screen, but all I got was dead-boring hierarchies of shapes, animals and employees with short, mostly one-liner methods. Defining your own Matrix class, overloading the "+" operator and doing "m1 + m2" was advertised as cool and exciting. If there was a minimal, quintessential example I absorbed, it was this:

class Shape {
public:
    virtual float area() = 0;
};

class Square: public Shape {
private:
    float side;

public:
    Square(float side): side(side) {};

    float area() {
        return side * side;
    }
};

class Rectangle: public Shape {
private:
    float width, height;

public:
    Rectangle(float width, float height): width(width), height(height) {}

    float area() {
        return width * height;
    }
};

Then they showed you how to instantiate and use these hierarchies:

Square *sq = new Square(5);
Rectangle *rect = new Rectangle(3, 4);

vector<Shape *> shapes = vector<Shape *>();
shapes.push_back(sq);
shapes.push_back(rect);

for (int i = 0; i < shapes.size(); i++) {
cout << shapes[i]->area() << endl;
}

shapes.clear();
delete sq;
delete rect;

And that was mostly it. At best, you learned how to implement a linked list. No Pong game or flying teapots on the screen. At that point, continuing playing with Lego looked more interesting and certainly more joyful than programming. There was never a word about any potential problems with that style of programming. Segfaults, leaks, dangling pointers, reading uninitialised data, overflows - there was the implicit assumption that good programmers don't make such mistakes. There was never the concept of "object ownership" as it is today. Passing objects between functions was an exercise of ad-hoc trickery. You had to learn the hard stuff by playing with the weird behaviours yourself.

Now, I would define the essence of modern C++ with transforming the above fragment into:

auto shapes = std::vector<std::unique_ptr<Shape>>();
shapes.push_back(std::make_unique<Square>(5));
shapes.push_back(std::make_unique<Rectangle>(3, 4));

for (const auto& shape: shapes) {
    cout << shape->area() << endl;
}

This code cannot leak and is exception-safe. The "pointers" are value objects with enforced semantics. Behind the scenes, move semantics transfers the ownership of the unique pointers from their temporary expressions to the containing vector. When the vector goes out of scope, everything else disappears safely.