make_unique versus new memory leak argument evaluation

Make Unique Exists Because Argument Evaluation Order Leaked Memory

I spent six years in high-frequency trading environments where a single leaked byte wasn’t just a “memory leak”—it was a line item on a loss report. I’ve sat through countless code reviews where senior devs argued over the syntactic sugar of make_unique versus new, treating it like a matter of aesthetic preference rather than a fundamental question of exception safety. Most tutorials treat this like a trivial choice for “cleaner code,” but they’re lying to you. If you’re still manually managing raw pointers in a multi-argument function call, you aren’t just writing “old school” C++; you are inviting non-deterministic crashes that the compiler will happily let slide right into production.

I’m not here to give you a lecture on the C++ standard or a list of stylistic best practices you’ll forget by Tuesday. I want to talk about what actually happens on the stack and the heap when an exception is thrown mid-allocation. I’m going to show you exactly where the ownership chain breaks and why choosing the wrong tool is a bug waiting to ship. We’ll look at the mechanics, the edge cases, and the real-world cost of being lazy with your allocations.

Table of Contents

The Exception Safety Trap Where Stdmake Unique Exception Safety Wins

The Exception Safety Trap Where Stdmake Unique Exception Safety Wins

The real danger isn’t just about clean syntax; it’s about what happens when the world falls apart mid-expression. Consider a function call where you’re passing two arguments to a constructor: `foo(std::unique_ptr(new T()), bar())`. If `bar()` throws an exception after `new T()` has already allocated memory but before the `unique_ptr` constructor takes ownership, you’ve just leaked memory. The pointer is floating in the void, and since the stack is unwinding, there is no mechanism left to catch it.

This is where std::make_unique exception safety becomes non-negotiable. By using `make_unique`, you collapse the allocation and the construction into a single, atomic step from the perspective of the calling code. There is no window of opportunity for an exception to strike between the moment the memory is grabbed and the moment the smart pointer assumes responsibility. When you follow these C++ memory allocation best practices, you aren’t just being pedantic; you are closing a gap in your error-handling logic that a debugger won’t necessarily help you find once the code is running in production.

Why Raw Pointer vs Unique Ptr Is a Dangerous Gamble

Why Raw Pointer vs Unique Ptr Is a Dangerous Gamble

The problem with treating raw pointers and `std::unique_ptr` as interchangeable is that they represent two entirely different philosophies of lifetime. A raw pointer is just an address; it has no inherent knowledge of whether it owns the memory it points to or if that memory is about to be pulled out from under it. When you pass a raw pointer around, you are essentially making a silent contract with every other function in your call stack, hoping they all agree on who is responsible for the cleanup.

In my experience, this ambiguity is where most production crashes live. If you rely on raw pointers for ownership, you’re playing a game of chance with ownership semantics in C++. One missed `delete` in a complex conditional branch, or one premature `delete` in a destructor, and you’ve either leaked memory or triggered a use-after-free. By switching to `std::unique_ptr`, you move that responsibility from a human’s fallible memory to the compiler’s deterministic logic. You aren’t just managing memory; you are encoding the lifecycle directly into the type system, making it much harder to ship a bug that only shows up under specific load conditions.

Rules of Engagement: How to Stop Guessing and Start Coding

  • Stop treating `new` as a valid way to initialize a smart pointer. If you find yourself writing `std::unique_ptr(new T())`, you’ve already lost the thread. Use `std::make_unique` to ensure the allocation and the ownership assignment happen in a single, atomic-feeling step that the compiler can actually reason about.
  • Watch your function arguments. If you pass `new T()` as an argument to a function alongside another expression that might throw, you’ve just created a leak that no static analyzer will catch in time. `std::make_unique` encapsulates that risk; raw `new` invites it.
  • Respect the type deduction. One of the few real wins for `make_unique` is that you don’t have to repeat the type name twice. It keeps the call site clean and reduces the surface area for typos that lead to mismatched pointer types.
  • Don’t use `make_unique` for everything if you need custom deleters. This is a common trap. `std::make_unique` is optimized for the standard `delete` path. If you’re managing a resource that requires a specific cleanup routine—like a C-style file handle—you’ll have to go back to the constructor. Know when you’re in standard territory and when you’re not.
  • Audit your legacy code for “naked” `new` calls. If you see `new` sitting there without an immediate wrap into a smart pointer, that’s a technical debt interest rate that will eventually compound into a production crash. Refactor it to `make_unique` immediately; don’t wait for the next sprint.

The Bottom Line

Stop treating `new` like a tool; treat it like a liability. If you aren’t manually managing a raw pointer, you shouldn’t be touching `new` at all.

Exception safety isn’t optional. Using `new` in a function argument list is a gamble that an exception will eventually leak memory, and `std::make_unique` eliminates that entire class of failure.

Let the type system do the heavy lifting. `std::unique_ptr` communicates intent and ownership to both the compiler and your teammates; raw pointers only communicate ambiguity.

The Bottom Line

At the end of the day, the choice isn’t about being “modern” for the sake of a trend; it’s about narrowing the gap between what you think your code is doing and what the machine actually executes. Using `new` forces you to manually manage the lifecycle of an object, creating a window of vulnerability where an exception can leak memory before you even realize it. By sticking to `std::make_unique`, you aren’t just following a style guide—you are leveraging the type system to enforce exception safety and clear ownership. It turns a potential runtime disaster into a compile-time certainty.

C++ is a language that gives you enough rope to hang yourself, but it also provides the tools to build something incredibly robust if you respect the underlying mechanics. Don’t write code that relies on your ability to remember every single edge case; write code that is mathematically harder to break. When you stop treating memory management as a chore and start treating it as a fundamental part of your system’s architecture, you stop shipping bugs and start shipping reliable software. Get back to the compiler, read the specs, and stop leaving your safety to chance.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Demonstrating const correctness in practice.

Const Is a Message to the Next Person Who Reads This Function

Understanding what move semantics actually move.

Moving Does Not Move Bytes, It Transfers Responsibility