17 July 2026
If you are considering learning C++ today, you have likely heard the arguments against it. Memory safety issues, buffer overflows, dangling pointers, and undefined behavior have made C++ a frequent target of criticism from security researchers and language advocates. Languages like Rust, Go, and even modern Swift offer memory safety guarantees that C++ simply cannot provide without extreme discipline. Yet C++ remains one of the most widely used systems programming languages in the world, powering everything from game engines and web browsers to financial trading systems and embedded devices. So why would anyone choose to learn it now?
The honest answer is that C++ still occupies a unique space that no other language fully covers. Understanding that space, and knowing when C++ is the right tool versus when it is a dangerous liability, is what separates a competent programmer from a great one. This article will walk you through the realities of learning C++ today, the trade-offs you must accept, and how to use the language responsibly in a world that increasingly demands safety.

Rust comes close, and in many cases it is superior. But Rust is not a drop-in replacement for C++. The C++ ecosystem includes decades of libraries, tooling, and domain-specific knowledge that cannot be replaced overnight. The Unreal Engine, the Godot engine, the Chromium browser, the LLVM compiler infrastructure, and countless embedded systems all rely on C++. Learning C++ opens doors to contributing to these projects and understanding how they work at a fundamental level.
Beyond that, C++ teaches you things that no safe language can. When you wrestle with manual memory management, you develop an intuition for how computers actually work. You learn about cache lines, virtual memory, stack vs heap allocation, and the cost of indirection. These lessons make you a better programmer in any language, because you understand the hardware constraints that higher-level abstractions hide.
The C++ standard library provides safer abstractions like `std::vector`, `std::string`, and smart pointers, but these are not enforced by the compiler. You can still call `.data()` on a vector and pass the raw pointer to a C function that writes beyond the allocated size. You can still use `std::shared_ptr` in a way that creates reference cycles and memory leaks. Safety in C++ is a matter of discipline, convention, and tooling, not language guarantees.
This is the fundamental trade-off. C++ trusts you. It assumes you know what you are doing. If you do, you can write extremely efficient and reliable code. If you do not, you will produce software that fails in mysterious and dangerous ways.

Misconception 1: Modern C++ is safe C++. This is partially true but misleading. Modern C++ encourages the use of RAII (Resource Acquisition Is Initialization), smart pointers, and containers, which eliminate many of the classic pitfalls of raw C-style programming. However, modern C++ does not eliminate undefined behavior. You can still have iterator invalidation, dangling references from lambdas, and data races in multithreaded code. The language standard itself contains hundreds of undefined behaviors that the compiler is free to exploit for optimization. Writing modern C++ reduces the surface area for bugs, but it does not eliminate it.
Misconception 2: You must use raw pointers everywhere. Many developers assume that C++ programming is synonymous with manual memory management. That is not true. In modern C++, you should rarely need to call `new` and `delete` directly. Use `std::unique_ptr` for exclusive ownership, `std::shared_ptr` for shared ownership (but sparingly), and `std::vector` or `std::array` for contiguous memory. Raw pointers should only appear as non-owning observers, and even then you should prefer references or `std::optional` where possible.
Misconception 3: C++ is impossible to write safely. This is defeatist and incorrect. Many large-scale C++ projects have remarkably low defect rates. The key is rigorous code review, static analysis, extensive testing, and a culture of safety. The Chromium project, for example, has invested heavily in sandboxing, fuzzing, and memory safety tooling. They still have bugs, but the rate is manageable. Safety is a process, not a property of the language alone.
For example, instead of this:
cpp
void process() {
int* data = new int[100];
// ... do something that might throw ...
delete[] data;
}
Write this:
cpp
void process() {
std::vector data(100);
// ... do something that might throw ...
// data is automatically freed when the function exits
}
The second version is shorter, clearer, and exception-safe. There is no reason to use raw `new` and `delete` in most application code.
Pay special attention to `std::unique_ptr` and `std::shared_ptr`. `unique_ptr` is zero-cost at runtime and enforces single ownership. `shared_ptr` uses reference counting and has overhead, but it is useful for shared ownership scenarios. However, be aware that `shared_ptr` can cause reference cycles if you use raw pointers or `weak_ptr` incorrectly. Always prefer `unique_ptr` unless you have a clear reason to share ownership.
You cannot rely on what your program does in the presence of undefined behavior. It might crash, it might appear to work, or it might work on your machine but fail on another. The only correct approach is to avoid undefined behavior entirely. Use compiler flags like `-Wall -Wextra -Wpedantic` and enable sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer) during development. These tools catch many common mistakes at runtime.
Use C++ when:
- You need maximum performance with predictable latency.
- You are working on an existing C++ codebase.
- You need to interface with hardware or operating system APIs that expect C-style interfaces.
- You are building a game engine, a real-time system, or a high-performance library where every microsecond counts.
- You need to control memory layout precisely, for example in embedded systems or graphics programming.
Avoid C++ when:
- You are building a web application or a REST API. Use Go, Rust, or a managed language like Java or C#.
- You are prototyping a new idea and need rapid iteration. Use Python or JavaScript.
- You are building a system where memory safety is paramount and you cannot afford the risk of human error. Use Rust.
- You are working on a team that does not have deep C++ expertise. The cost of debugging memory bugs can be enormous.
This is not a binary choice. Many projects use multiple languages. You might write performance-critical components in C++ and the rest in a safer language. The key is to be honest about the trade-offs and not default to C++ just because you know it.
Scenario 1: A high-frequency trading system. Latency is measured in nanoseconds. Every memory allocation, every virtual function call, every cache miss costs money. In this environment, you need to preallocate memory pools, use lock-free data structures, and carefully control instruction ordering. No garbage-collected language can provide the necessary guarantees. C++ is the right choice here, but you must be extremely disciplined. Use static analysis, fuzzing, and rigorous testing. Even then, bugs can be catastrophic.
Scenario 2: A command-line tool for parsing JSON. You could write this in C++, but you would gain very little. The performance difference between C++ and Rust or Go for this task is negligible for most use cases. The safety overhead of Rust's borrow checker or Go's garbage collector is not a problem. You would be better served by a language that eliminates entire classes of bugs from the start. Choose Rust if you want memory safety without a runtime, or Go if you want simplicity and fast compilation.
The mistake many developers make is assuming that C++ is always faster. It is not. The cost of manual memory management, debugging memory corruption, and dealing with undefined behavior can easily outweigh the performance benefits for most applications. Only use C++ when the performance requirements are extreme enough to justify the risk.
Mistake 1: Using `new` and `delete` in application code. There is almost never a reason to do this. Use `std::make_unique` and `std::make_shared` instead. They are safer and more concise.
Mistake 2: Ignoring iterator invalidation. When you insert or erase elements from a `std::vector`, all iterators, pointers, and references to elements after the modification point become invalid. Using them is undefined behavior. Be careful when writing loops that modify containers.
Mistake 3: Overusing `std::shared_ptr`. Shared pointers are convenient, but they have overhead and can hide ownership problems. Prefer `std::unique_ptr` and pass raw pointers or references as non-owning observers. If you find yourself using `shared_ptr` everywhere, reconsider your design.
Mistake 4: Not understanding exception safety. If a function throws an exception, all resources must be correctly released. RAII handles this automatically, but if you manually manage resources, you must write exception-safe code. The rule of thumb is: if you write a `new`, you need a corresponding `delete` in a destructor, and that destructor must be called even if an exception is thrown.
Mistake 5: Assuming that "it works on my machine" means it is correct. Undefined behavior can appear to work correctly in one environment and fail in another. The compiler is allowed to assume undefined behavior never occurs, so your program might be silently broken. Always run sanitizers and test on multiple platforms.
However, C++ will never be as safe as Rust by design. The language's commitment to backward compatibility and zero-cost abstractions means that some unsafe features will always exist. The best you can do is use the safe subset of the language and enforce it with tooling.
If you are considering learning C++ today, do it because you want to understand systems programming at a deep level, because you need to work on a specific codebase, or because you enjoy the challenge. Do not do it because you think it is the only way to write fast software. In many cases, Rust, Go, or even C with discipline will serve you better.
Approach C++ with humility. Accept that you will make mistakes. Use every tool available to catch those mistakes early. Write code that is simple, clear, and follows established conventions. When in doubt, prefer the safer abstraction. And never forget that the goal is not to write clever code, but to write correct and maintainable software.
The safer alternatives are not your enemies. They are your teachers. Rust teaches you about ownership and lifetimes. Go teaches you about simplicity and concurrency. Python teaches you about expressiveness and rapid development. Learn from them, and bring those lessons back to your C++ work.
If you decide to learn C++, you will gain a deep understanding of how computers work, a skill that transfers to every other language. You will also gain a healthy respect for the complexity of systems programming. That respect, more than any tool or technique, is what will make you a better programmer.
all images in this post were generated using AI tools
Category:
Programming LanguagesAuthor:
John Peterson