Question

Why C++ Programmers Should Minimize Use of new

cppmemory-managementheap-memorynew-operatorc++-faq

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:

  1. Create objects directly by value when possible.
  2. If shared or dynamic lifetime is needed, use smart pointers such as std::unique_ptr or std::shared_ptr.
  3. Avoid raw new and delete in 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 new is just a note containing an address

Beginners often focus on the address note and forget they now have to manage the real object manually.

Take Quiz

Syntax and Examples

Basic syntax

Create an object directly

std::string message = "hello";
  • message is a real std::string object.
  • It is destroyed automatically when it goes out of scope.

Create an object with new

std::string* message = new std::string("hello");
  • message is a pointer.
  • The actual std::string lives 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

  1. std::string a = "hello";

    • A std::string object named a is created directly.
    • Its lifetime is tied to the scope of main.
  2. std::string* b = new std::string("world");

    • Memory is allocated dynamically.
    • A std::string object is constructed in that memory.
    • b stores the address of that object.
  3. std::cout << a << '\n';

    • Prints the direct object.
  4. std::cout << *b << '\n';

    • b is a pointer, so you must dereference it with *b to access the string.
  5. delete b;

    • The std::string destructor runs.
    • The dynamic memory is released.
  6. End of main

    • a is destroyed automatically.
    • b itself 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."

Take Quiz

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

ApproachWhat you getCleanupTypical useRisk level
Direct object by valueThe actual objectAutomaticLocal variables, containers, return valuesLow
Raw pointer with newPointer to heap objectManual deleteRare low-level code, legacy APIsHigh
std::unique_ptrOwning pointer to heap objectAutomaticDynamic lifetime with single ownershipLow
std::shared_ptrShared ownership of heap objectAutomatic when last owner diesShared lifetime across componentsMedium

By value vs raw new

TopicBy valueRaw new
OwnershipUsually obviousOften unclear
CleanupAutomaticManual
Exception safetyStronger by defaultEasy to get wrong
SyntaxSimplerMore verbose
PerformanceOften very goodHeap allocation may cost more

Raw pointer vs smart pointer

FeatureRaw pointerstd::unique_ptr
Owns object?Not clear from type aloneYes
Deletes automatically?NoYes
Safe to return ownership?Error-proneYes
Good default for ownership?NoYes

The main comparison is not "stack versus heap" in a simplistic sense. The more important difference is automatic lifetime management versus manual lifetime management.

Take Quiz

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_ptr when dynamic allocation is needed.
  • Use std::shared_ptr only when shared ownership is truly required.
  • Avoid raw new and delete in normal application code.

Key ideas

  • new allocates memory and constructs an object.
  • delete destroys 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.

Take Quiz

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 Task type with a title and completion status.
  • Store multiple Task objects in a standard container.
  • Add at least three tasks.
  • Print each task in a readable format.
  • Do not use raw new or delete.
Take Quiz

Keep learning

Related questions

View more