Skip to main content

Command Palette

Search for a command to run...

Modern C++: Stop Writing 'C with Classes'

Why modern C++ enforces better habits than its predecessors

Updated
6 min read
Modern C++: Stop Writing 'C with Classes'
S

Since I was a kid, always liked computer security and the programming world. Now I am constantly learning new stuff :)

At the first year of University they taught me C and later on C++. While it is a hard language to learn as the first one, it has an incredible advantage over some others. It teaches you to think about dealing with memory. But you already knew that…

The problem? Most universities and (online tutorials) teach you C++ as if it were “C with classes tacked on top”. You learn new and delete, raw pointers everywhere, manual memory management… Basically C with some extra facilities.

And that’s where things get dangerous.

And by teaching you this they are not doing anything wrong, learning this is fundamental; but, C++ is constantly evolving and safety-practices are also.


The “C with Classes” Trap

A quick example about what I mean:

class Buffer {
    char* data;
    int size;

public:
    Buffer(int s) {
        size = s;
        data = new char[size];
    }

    ~Buffer() {
        delete[] data;
    }

    char* getData() { return data; }
};

It doesn’t look wrong, right? There are classes, encapsulation, a destructor…

Well, actually, this is a minefield:

  1. No copy constructor: Without a copy constructor, copying this object leads to undefined behavior, most likely a double free.

  2. No move constructor: More inefficient.

  3. Raw pointer exposed: With “char* getData()…” you are exposing a raw pointer to the internal memory. This means external code has full control over the buffer.

  4. No bounds checking: Potential buffer overflow.

  5. Manual memory management: Easier having a memory leak.

This is “C with classes.” You’re using class syntax but still thinking in C. And in reality this stuff gets exploited constantly.


The Modern Way (C++ 11 and Up)

class Buffer {
    std::vector<char> data;

public:
    Buffer(size_t size) : data(size) {}

    std::span<char> getData() { return data; }
    char& at(size_t pos) { return data.at(pos); }
};

Now:

  • No manual memory management → std::vector handles it.

  • Copy/move automatically generated → compiler does it right.

  • Bounds checking → now the “.at()” throws an exception on overflow.

  • Exception safe → cleanup happens automatically.

  • Can’t leak memory → impossible by how it’s designed.

The modern version is shorter and safer.


Core Modern C++ Concepts You Need

C++ has some concepts that are important to get right.

1. RAII (Resource Acquisition Is Initialization)

This is THE fundamental concept in modern C++. Master this, and everything else falls into place.

  • Resources → (memory, files, locks) are acquired in constructors.

  • Resources are released in destructors.

  • Stack unwinding guarantees cleanup, even during exceptions.

Example:

void processFile() {
    std::ifstream file(”data.txt”);  // Opens in constructor
    // ... use file ...
    // File closes automatically when scope ends, even if exception thrown
}

Now you have no risk about forgetting “fclose()” :).

2. Smart Pointers vs Raw Pointers

What are smart pointers? A smart pointer is a class that wraps a raw pointer and manages its lifetime automatically. That’s it. Check here for extended documentation.

When to use each type:

  • std::unique_ptr<T> → Single owner, the most common. You will be using it most of the time.
  • std::shared_ptr<T> → Shared ownership via reference counting; use only when ownership must be shared.

  • std::weak_ptr<T> → Non-owning reference to shared_ptr (breaks cycles).

  • Raw pointers (T*) → Only for non-owning references where lifetime is guaranteed externally.

Example:

class CWindow {
    std::unique_ptr<CTexture> m_pWindowTexture;  // Window owns texture
    CMonitor* m_pMonitor;  // Window doesn’t own monitor, just references it
};

Btw, this code is from Hyprland GitHub repo.

The pointer type tells you the ownership.

3. std::optional vs Error Codes

Old way (C with classes):

int parseConfig(const char* path, Config* out) {
    if (!path) return -1;
    if (!out) return -2;
    // ... parse ...
    return 0;  // Success
}

// Caller can ignore the error.
Config cfg;
parseConfig(”file.ini”, &cfg);  // Ignored return value - BUG

Modern way:

std::optional<Config> parseConfig(std::string_view path) {
    // ... parse ...
    if (success) {
        return Config{...};
    }
    return std::nullopt;  // Explicit failure
}

// Compiler warns if you don’t check.
auto cfg = parseConfig(”file.ini”);
if (cfg) {
    // Use cfg.value()
}

The type system forces you to handle failure.

4. Move Semantics (C++11)

This is huge for performance without sacrificing safety:

std::vector<int> createLargeVector() {
    std::vector<int> v(1000000);
    // ... fill vector ...
    return v;  // Moved, not copied. (C++ 11)
}

auto data = createLargeVector();  // No copy, just a pointer swap

Before C++ 11, this would have copied 1 million integers, now there are no copies, the internal buffer is moved.


But Modern C++ Is Slower, right?

Right???

Well wrong! This is a common myth on the internet. I guess people see std::unique_ptr and think “that is an extra overhead compared to raw pointers“.

So what actually happens:

// Raw pointer
Widget* ptr = new Widget();
delete ptr;

// Smart pointer
std::unique_ptr<Widget> ptr = std::make_unique<Widget>();

And what about the generated assembly?
To test this I did it with a website called Compiler Explorer, here is the link to see it.

Used the flag [-O2 -std=c++20] for optimizations.

Modern C++ isn’t just “not slower”, it’s often faster than manual memory management:

  1. Move semantics avoid copies (we’ve seen this before)

  2. Compilers optimize STL heavily

  3. Cache locality std::vector stores elements contiguously.

From the Real World

I’ve been learning about the Hyprland codebase, since I want to contribute and make some plugins for my Linux Desktop; and std::unique_ptr is nearly everywhere.
Hyprland might not be the best example, but is a good one for demonstrating modern C++.

If smart pointers really had overhead, Hyprland would not be one of the fastest compositors.

When There IS Overhead

To be fair, there are cases with actual cost:

std::shared_ptr has overhead:

  • Reference counting (atomic increments/decrements).

  • Extra allocation for the control block.

  • Use only when you actually need shared ownership.

std::function has overhead:

  • Type erasure = virtual call.

  • Small buffer optimization helps, but it’s there.

But std::unique_ptr has zero cost. It’s an Abstraction.

The Real Cost Are Bugs

Here’s the thing: even if smart pointers had a tiny overhead (they don’t), bugs are way more expensive.

The time spent debugging is pretty damn big for fixing it.

  • Memory leak → hours tracking it down

  • Use-after-free → could be days, could be never

  • Double-free → good luck reproducing it

  • Time spent with smart pointers = Zero bugs from memory management, or at least close to it.


Wrapping Up

If you’re learning C++ today, learn the fundamentals first, then come back to this post and apply these concepts to your previous programs.

Simple summary:

  • Smart pointers eliminate memory bugs.

  • RAII guarantees cleanup.

  • std::optional makes errors explicit.

  • Move semantics gives you performance.


Found this helpful? Share it with someone learning C++.

Some stuff that is widely recommended for C++:

Effective Modern C++ by Scott Meyers

cppreference.com This is what you will use more.

Hyprland source code for some examples I’ve used. (And hyprland is also so cool).


I’ll probably be writing more about C++ on this blog while I am learning new concepts.

Modern C++: Stop Writing 'C with Classes'