Question
Why C++ Programmers Should Minimize Use of new
Question
In C++, why is it often recommended to avoid using new unless it is truly necessary?
I saw advice saying that many objects should be created by value instead of allocating them on the heap with new. The suggestion was that this is a major advantage of C++, and that not everything needs dynamic allocation.
I want to understand what this means in practice:
- Why should C++ programmers create objects by value as often as possible?
- What is the internal difference between creating an object normally and creating it with
new? - How does this affect memory management, performance, and correctness?
- Is the advice mainly about avoiding memory leaks, or is there a broader design reason behind it?
For example, what is the practical difference between code like this?
std::string a = "hello";
std::string* b = new std::string("hello");
Did I misunderstand the advice, or is this a general C++ best practice?
Short Answer
By the end of this page, you will understand why modern C++ usually prefers creating objects directly rather than with raw new. You will learn the difference between automatic storage and dynamic storage, how RAII makes cleanup automatic, why raw pointers often lead to leaks and complexity, and when dynamic allocation is still the right tool.
Concept
In C++, new creates an object in dynamic storage (commonly called the heap) and returns a pointer to it. That object stays alive until you manually destroy it with delete.
Creating an object by value usually means declaring it directly:
std::string name = "hello";
This creates the object itself, not a pointer to it. Very often, such an object has automatic storage duration, meaning it is cleaned up automatically when it goes out of scope.
{
std::string name = "hello";
} // name is destroyed automatically here
This matters because C++ is built around resource management through object lifetime. A core idiom is RAII: Resource Acquisition Is Initialization. An object acquires a resource in its constructor and releases it in its destructor. If the object is created directly, cleanup is automatic and tied to scope.
When you use raw new, you separate:
- object creation
- object ownership
- object destruction
That separation creates risk:
- forgetting
delete - deleting twice
- deleting at the wrong time
- unclear ownership
- exception safety problems
So the advice is broader than just "avoid leaks." It is about writing code that is:
- safer
- easier to reason about
- more exception-safe
- often faster
- more idiomatic in C++
In modern C++, the usual preference is:
- Create objects directly by value when possible.
- If shared or dynamic lifetime is needed, use smart pointers such as
std::unique_ptrorstd::shared_ptr. - Avoid raw
newanddeletein application code unless you are implementing a low-level abstraction.
For example:
std::string a = "hello"; // preferred in most cases
auto b = std::make_unique<std::string>("hello"); // if dynamic lifetime is needed
The key idea is that C++ lets objects manage themselves through scope and destructors. Using raw new too often ignores one of the language's biggest strengths.
Mental Model
Think of C++ objects like tools you borrow for a task.
- By value: you take the tool into your workspace. When you leave the room, the tool is automatically returned.
- With
new: you rent a tool from an external storage unit. Now you must remember to return it manually later.
If you rent everything manually, problems appear:
- you forget to return a tool
- two people think they own the same tool
- someone returns it too early
- someone keeps using it after it was returned
Creating objects by value means C++ keeps the cleanup tied to scope. That makes ownership obvious.
Another way to think about it:
- a normal object is the actual thing
- a pointer from
newis just a note containing an address
Beginners often focus on the address note and forget they now have to manage the real object manually.
Syntax and Examples
Basic syntax
Create an object directly
std::string message = "hello";
messageis a realstd::stringobject.- It is destroyed automatically when it goes out of scope.
Create an object with new
std::string* message = new std::string("hello");
messageis a pointer.- The actual
std::stringlives in dynamic storage. - You must later write:
delete message;
Preferred: value semantics
#include <iostream>
#include <string>
int main() {
std::string name = "Alice";
std::cout << name << '\n';
}
Why this is good:
- simple
- no manual cleanup
- no leak risk
- clear ownership
Problematic raw new
#include <iostream>
#include <string>
int main() {
std::string* name = new std::string("Alice");
std::cout << *name << '\n';
delete name;
}
This works, but it is more fragile because:
- you must remember
delete - exceptions can skip cleanup
- the pointer can be copied, making ownership unclear
Better when dynamic allocation is needed
#include <iostream>
#include <memory>
#include <string>
int main() {
auto name = std::make_unique<std::string>("Alice");
std::cout << *name << '\n';
}
This still uses dynamic allocation, but cleanup is automatic because std::unique_ptr deletes the object when it goes out of scope.
Objects inside containers
In C++, containers usually store objects directly:
#include <list>
#include <string>
int main() {
std::list<std::string> words;
words.push_back("one");
words.push_back("two");
}
This is usually better than storing pointers:
std::list<std::string*> words;
words.push_back(new std::string("one"));
words.push_back(new std::string("two"));
The second version now requires manual deletion for every element, which is error-prone.
Step by Step Execution
Consider this example:
#include <iostream>
#include <string>
int main() {
std::string a = "hello";
std::string* b = new std::string("world");
std::cout << a << '\n';
std::cout << *b << '\n';
delete b;
}
Step-by-step
-
std::string a = "hello";- A
std::stringobject namedais created directly. - Its lifetime is tied to the scope of
main.
- A
-
std::string* b = new std::string("world");- Memory is allocated dynamically.
- A
std::stringobject is constructed in that memory. bstores the address of that object.
-
std::cout << a << '\n';- Prints the direct object.
-
std::cout << *b << '\n';bis a pointer, so you must dereference it with*bto access the string.
-
delete b;- The
std::stringdestructor runs. - The dynamic memory is released.
- The
-
End of
mainais destroyed automatically.bitself is just a pointer variable; it goes away too.
Important observation
If step 5 is forgotten:
std::string* b = new std::string("world");
then the object remains allocated after b goes out of scope. That is a memory leak.
Exception safety example
void f() {
std::string* p = new std::string("data");
throw std::runtime_error("error");
delete p;
}
Here, delete p; is never reached, so the memory leaks.
With value semantics:
void f() {
std::string p = "data";
throw std::runtime_error("error");
}
No leak occurs. During stack unwinding, p is destroyed automatically.
Real World Use Cases
Creating objects by value is common in real C++ programs because most objects have a clear local lifetime.
Typical cases for by-value objects
Local variables in functions
std::string buildMessage() {
std::string msg = "ready";
return msg;
}
Used for:
- temporary results
- parsing data
- formatting strings
- business logic
Containers of objects
std::vector<std::string> names;
names.push_back("Ada");
names.push_back("Linus");
Used for:
- records
- messages
- model objects
- parsed tokens
Resource-owning wrapper objects
std::ofstream file("output.txt");
Used for:
- files
- mutex locks
- sockets via wrapper types
- database handles via wrapper classes
These work well because cleanup happens in destructors.
Cases where dynamic allocation is useful
Dynamic allocation is still important when:
- an object must outlive the current scope
- the size or lifetime is not known at compile time
- polymorphism is needed through base-class pointers or references
- ownership must be transferred explicitly
- large object graphs are being built
Example:
std::unique_ptr<Base> obj = std::make_unique<Derived>();
The lesson is not "never allocate dynamically." The lesson is "do not use raw new by default."
Real Codebase Usage
In real projects, developers usually try to make ownership obvious.
Common patterns
1. Prefer local values
Config config;
Logger logger;
std::string path = "app.log";
This is the default style for local work.
2. Return objects by value
std::string readName();
User parseUser(const std::string& text);
Modern C++ optimizes value returns well, so returning objects by value is often efficient and clearer than returning raw pointers.
3. Use smart pointers for ownership
auto conn = std::make_unique<Connection>();
This shows one clear owner.
4. Use references or pointers for non-owning access
void printUser(const User& user);
If a function does not own an object, pass by reference or pointer rather than creating heap objects needlessly.
5. Store objects directly in containers
std::vector<Task> tasks;
Instead of:
std::vector<Task*> tasks;
unless polymorphism or special lifetime behavior is required.
6. Guard clauses and validation
Creating by value works especially well with early returns:
std::string normalize(const std::string& input) {
if (input.empty()) {
return "";
}
std::string result = input;
// transform result
return result;
}
No manual cleanup is needed no matter where the function returns.
Why teams prefer this style
- fewer ownership bugs
- easier reviews
- easier exception safety
- better integration with standard containers
- code expresses intent more clearly
Common Mistakes
1. Using new for ordinary local objects
Broken style:
void greet() {
std::string* s = new std::string("hello");
std::cout << *s << '\n';
delete s;
}
Better:
void greet() {
std::string s = "hello";
std::cout << s << '\n';
}
Avoid raw new unless dynamic lifetime is actually needed.
2. Forgetting delete
Broken code:
void f() {
int* p = new int(42);
}
This leaks memory.
Better:
void f() {
int p = 42;
}
or if dynamic allocation is required:
void f() {
auto p = std::make_unique<int>(42);
}
3. Mismatched ownership
Broken code:
std::string* makeName() {
return new std::string("Alice");
}
Who deletes the returned pointer? This is unclear and easy to misuse.
Better:
std::string makeName() {
return "Alice";
}
Or if ownership transfer is intended:
std::unique_ptr<std::string> makeName() {
return std::make_unique<std::string>("Alice");
}
4. Using raw pointers in containers without a cleanup strategy
Broken code:
std::vector<std::string*> items;
items.push_back(new std::string("a"));
items.push_back(new std::string("b"));
Now every element must be deleted manually.
Better:
std::vector<std::string> items;
items.push_back("a");
items.push_back("b");
5. Assuming new is always faster or more powerful
It is not automatically better. Heap allocation often adds overhead and complexity.
6. Confusing pointer lifetime with object lifetime
Broken thinking:
std::string* p = new std::string("hi");
If p goes out of scope, the pointed-to object does not get deleted automatically.
The pointer variable and the heap object have separate lifetimes.
Comparisons
| Approach | What you get | Cleanup | Typical use | Risk level |
|---|---|---|---|---|
| Direct object by value | The actual object | Automatic | Local variables, containers, return values | Low |
Raw pointer with new | Pointer to heap object | Manual delete | Rare low-level code, legacy APIs | High |
std::unique_ptr | Owning pointer to heap object | Automatic | Dynamic lifetime with single ownership | Low |
std::shared_ptr | Shared ownership of heap object | Automatic when last owner dies | Shared lifetime across components | Medium |
By value vs raw new
| Topic | By value | Raw new |
|---|---|---|
| Ownership | Usually obvious | Often unclear |
| Cleanup | Automatic | Manual |
| Exception safety | Stronger by default | Easy to get wrong |
| Syntax | Simpler | More verbose |
| Performance | Often very good | Heap allocation may cost more |
Raw pointer vs smart pointer
| Feature | Raw pointer | std::unique_ptr |
|---|---|---|
| Owns object? | Not clear from type alone | Yes |
| Deletes automatically? | No | Yes |
| Safe to return ownership? | Error-prone | Yes |
| Good default for ownership? | No | Yes |
The main comparison is not "stack versus heap" in a simplistic sense. The more important difference is automatic lifetime management versus manual lifetime management.
Cheat Sheet
// Preferred local object
std::string s = "hello";
// Raw dynamic allocation (usually avoid)
std::string* p = new std::string("hello");
delete p;
// Preferred dynamic allocation
auto p2 = std::make_unique<std::string>("hello");
Rules of thumb
- Prefer creating objects directly.
- Prefer storing objects, not owning raw pointers.
- Prefer returning objects by value.
- Use
std::unique_ptrwhen dynamic allocation is needed. - Use
std::shared_ptronly when shared ownership is truly required. - Avoid raw
newanddeletein normal application code.
Key ideas
newallocates memory and constructs an object.deletedestroys the object and frees the memory.- A local object is destroyed automatically at end of scope.
- A pointer going out of scope does not delete the object it points to.
- Containers like
std::vector<T>usually work best with actual objects, not raw pointers.
Edge cases
- Dynamic allocation is valid for polymorphism, long-lived objects, or runtime-managed ownership.
- Not every object lives on the stack just because you wrote it directly; storage duration depends on context.
- The important principle is managing lifetime safely and clearly.
FAQ
Why is new discouraged in modern C++?
Because raw new requires manual delete, which makes leaks, double deletes, and ownership bugs more likely.
Is creating objects by value always on the stack?
Not always. The common local case uses automatic storage, but the main idea is direct object management and automatic lifetime, not just physical memory location.
Is new ever necessary in C++?
Yes. It is useful when an object needs dynamic lifetime, polymorphic ownership, or runtime-managed allocation. Even then, smart pointers are usually preferred over raw new.
Why is returning objects by value okay in C++?
Modern C++ optimizes value returns well, and the code is usually simpler and safer than returning raw pointers.
Are raw pointers bad?
Not inherently. They are fine for non-owning access. The main problem is using raw pointers to represent ownership.
What should I use instead of new?
Usually a direct object. If dynamic allocation is required, use std::make_unique or sometimes std::make_shared.
Does avoiding new improve performance?
Often yes, because heap allocation can be slower and adds management overhead. But correctness and simplicity are usually the bigger reasons.
Why do standard containers prefer storing objects directly?
Because they manage object lifetimes correctly and efficiently, and direct storage avoids manual cleanup logic.
Mini Project
Description
Build a small C++ program that manages a list of tasks. The goal is to practice storing objects directly in a container instead of allocating each task with raw new. This demonstrates safer ownership, simpler cleanup, and more idiomatic C++ design.
Goal
Create a task list program that stores task objects by value and prints all tasks without using raw new or delete.
Requirements
- Define a
Tasktype with a title and completion status. - Store multiple
Taskobjects in a standard container. - Add at least three tasks.
- Print each task in a readable format.
- Do not use raw
newordelete.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.