C++ / MEMORY OWNERSHIP AND SMART POINTERS
Why raw new is rare in modern code
Rewrite code that calls new into value, container, or make_unique form, and name the few places where a raw new is still correct.
What you will learn
- Turn new T(args) into std::make_unique<T>(args) and drop the matching delete
- Turn new T[n] into std::vector<T>, which remembers n and frees itself on unwind
- Return unique_ptr or a value instead of T*, so the signature states who deletes
- Name the cases where raw new stays: owner internals, placement new, C API edges
Understanding Why raw new is rare in modern code
Every new expression hands back a pointer whose type, `Widget*`, records an address and nothing else. It does not say whether the callee, the caller, or nobody is responsible for the matching delete, whether one object or a thousand live at that address, or whether the pointer is still valid. Modern C++ does not avoid new because allocation is expensive or suspicious; it avoids it because `Widget*` is the wrong type to pass around. `std::unique_ptr<Widget>`, `std::vector<int>`, and `std::string` answer those questions inside the type, where the compiler and a destructor can act on the answer.
The allocations did not disappear, they moved. `std::make_unique<Widget>(args)` calls the same operator new, and a vector growing past its capacity does too. What changed is the length of the window between "memory exists" and "something owns it": with a bare new that window spans at least one statement, and any throw, early return, or break inside it leaks, while make_unique and container constructors close the window inside a function that is already exception-safe. Two habits travel with this: `new T[n]` becomes `std::vector<T>` because new[] forgets the count and pairing it with plain delete is undefined behaviour, and returning a large object by value now costs a pointer handoff rather than a copy, so heap-allocating to dodge a copy buys nothing.
Raw new still has a few homes, all of them places where you write the ownership rule once instead of at every call site: inside a container or intrusive node type you are implementing, in placement-new form when constructing into storage you already own, and at the edge of a C API whose pointers must be adopted or released. A useful review test is that a new in application code is a defect unless the resulting pointer is consumed by an owner in the same statement, because there is no second statement in which the program is still correct if something throws. `std::unique_ptr<Widget> p(new Widget)` compiles and works, but it spells Widget twice and puts a raw pointer back into an expression, which is why make_unique is the default and make_shared is the same argument for shared ownership.
<iostream>
<memory>
<string>
<utility>
<vector>
struct Part {
std::string id;
explicit Part(std::string s) : id(std::move(s)) {
std::cout << "made " << id << '\n';
}
~Part() { std::cout << "freed " << id << '\n'; }
};
// In 1998 this returned Part*, and the signature could not say who deletes it.
std::unique_ptr<Part> makePart(std::string id) {
return std::make_unique<Part>(std::move(id));
}
int main() {
std::vector<std::unique_ptr<Part>> parts;
parts.push_back(makePart("gear"));
parts.push_back(makePart("spring"));
std::cout << "held " << parts.size() << '\n';
parts.pop_back();
std::cout << "held " << parts.size() << '\n';
return 0; // no new and no delete anywhere in this file
}
A heap allocation should be owned by a named type from the instant it exists, and new produces one that nothing owns, so the call belongs inside make_unique, a container, or a wrapper you write once.
Worked examples
An array becomes a vector
Shows what a vector knows that a new[] pointer does not, including during stack unwinding.
<cstddef>
<iostream>
<numeric>
<stdexcept>
<vector>
std::vector<int> readSamples(std::size_t n) {
std::vector<int> v(n);
std::iota(v.begin(), v.end(), 1);
if (n > 3) throw std::runtime_error("device overflow");
return v;
}
int main() {
try {
std::vector<int> ok = readSamples(3);
std::cout << "sum " << std::accumulate(ok.begin(), ok.end(), 0) << '\n';
std::vector<int> bad = readSamples(5);
std::cout << "unreachable " << bad.size() << '\n';
} catch (const std::exception& e) {
std::cout << "caught " << e.what() << '\n';
}
std::cout << "nothing to delete[]\n";
}
Example explained
Line 1`std::vector<int> v(n)` allocates n ints and stores n, so the extent travels with the object, while `new int[n]` returns a pointer that has forgotten n.
Line 2The throw unwinds out of readSamples and v's destructor frees the buffer; a hand-written `delete[] p;` placed after the throw point would never execute.
Line 3`return v;` moves or elides, so the caller receives the same heap block and there is no copy worth avoiding by allocating manually.
Line 4No line in the program has to choose between delete and delete[], which is the pairing that raw array new gets wrong.
Where a raw new legitimately survives
Adopting an owning pointer from a C-style library in the statement that produces it.
<iostream>
<memory>
<utility>
// A C-style library: the only new in the program lives in here.
namespace legacy {
struct Session { int id; };
Session* open(int id) { return new Session{id}; }
void close(Session* s) { std::cout << "close " << s->id << '\n'; delete s; }
}
struct SessionCloser {
void operator()(legacy::Session* s) const { legacy::close(s); }
};
using OwnedSession = std::unique_ptr<legacy::Session, SessionCloser>;
int main() {
OwnedSession s(legacy::open(7));
std::cout << "using " << s->id << '\n';
OwnedSession moved = std::move(s);
std::cout << "old handle empty: " << (s == nullptr) << '\n';
return 0;
}
Example explained
Line 1`legacy::open` is where a raw new still belongs: it sits inside the component that defines the release rule, not in the code that uses it.
Line 2`OwnedSession s(legacy::open(7));` adopts the pointer in the same statement that produced it, so no code path exists where the pointer is alive and unowned.
Line 3SessionCloser calls `legacy::close`, not delete, so the library's pairing rule is carried by the type instead of by the caller remembering it.
Line 4After the move `s` is null, which is why `close 7` prints exactly once, from `moved` being destroyed at the end of main.
Important notes
If the owner is typed on a base class, as in std::unique_ptr<Base> p = std::make_unique<Derived>(), Base needs a virtual destructor or the delete inside unique_ptr is undefined behaviour.
make_unique cannot supply a custom deleter and cannot forward a braced initializer list, so std::unique_ptr<T, D>(new T{...}, d) is one of the few spots where writing new yourself is still normal.
Common mistakes
Converting a function body to make_unique but keeping `T* f()` and returning `p.release()`; the pointer is ownerless again the moment it returns, so the leak simply moves to the caller.
Calling delete on a pointer obtained from `get()` while mixing old and new styles; the unique_ptr deletes the same block again at scope exit, and the double free usually crashes far from that line.
Wrapping everything in unique_ptr because new is "bad": a member that could be a plain T value now costs an extra allocation plus an indirection on every access.
Try it yourself
Change, predict, then run
Write a struct Node holding a std::string tag with a destructor that prints it, a factory that returns `new Node{...}`, and a std::vector<Node*> holding two nodes that you delete by hand. Then convert the factory to std::unique_ptr<Node> with make_unique and the container to std::vector<std::unique_ptr<Node>>, delete every delete, and check the destructor lines still print in the same order.
Open the C++ workspaceCheck your understanding
A factory is declared `Widget* build();` and today's only caller does remember to call delete. Why is `std::unique_ptr<Widget> build();` still considered the better declaration?
- Because the return type itself states that ownership moves to the caller, so a future caller cannot silently get it wrong
- Because make_unique avoids the heap allocation that new performs, making the factory cheaper
- Because returning unique_ptr copies the Widget into the caller's stack frame instead of using the heap
- Because raw pointers cannot refer to polymorphic objects, while unique_ptr can
Show answer
The unique_ptr return type moves "who deletes this" out of a comment and into the signature, where the compiler enforces it for callers written long after the factory. The claim that the allocation disappears is tempting because the word new is gone from the source, but make_unique performs exactly the same operator new call; the gain is enforced ownership, not fewer allocations.