Yes, you can write C++ programs that are statically type safe and have no resource leaks. You can do it simply, without loss of performance, and without limiting C++’s expressive power.

Leaking Resources: Memory allocation, deallocation and object lifetime safety have been a long time concern and a major source of errors in real-world C++ code. Leaking resources can be difficult to locate and can cause catastrophic failure of an application.

Resource leaks are generally due to programming errors: resources that have been acquired must be released, but since release often happens long after acquisition, and many things may occur in the meantime, including an exception being thrown or abnormal program termination, it's easy for the release of resources to be missed.

C++ gives you the power and flexibility to manually allocate memory and create pointers to objects on the free store (heap, dynamic memory) by using the keyword new.

// Free Store (Heap) allocated 
  Entity* ef = new Entity (42);

However, if your program doesn't delete every resource that was manually acquired with new, explicitly, these free store allocated objects can and do outlive the pointers that point to them, thereby causing memory leaks. Because they don't automatically get deleted when their raw pointer, which is on the stack, goes out of scope.

Objects can also be allocated using malloc() and deallocated using free() (or similar functions), but the ways to avoid the use of new and delete apply to those as well. Note: Although new and delete are typically implemented as wrappers around malloc and free, you should not mix the usage of new and delete with malloc, realloc or free.

For non-static variables, scope determines when they are created and destroyed in program memory.

For example, if you declare a variable x within a function, x is only visible within that function body. It has local scope.

void f() 
{
 int x{42};
} // End of scope

int main() 
{
 f();
// std::cout << x << '\n'; error: ‘x’ was not declared in this scope

  return 0;
}

Variables with local scope have a name declared within a function or lambda, including the parameter names. They are often referred to as “locals”. They are only visible from their point of declaration to the end of the function or lambda expression body. Local scope is a kind of block scope.

When you declare a class, function, or variable, its name can only be “seen” and used in certain parts of your program. The context in which a name is visible is called its scope.

The model of constructors and destructors and releasing resources at scope exit has been part of C++ since the earliest days.

Resource Acquisition Is Initialization (RAII)

Although memory is the most common resource that is in limited supply to your application, limited resources also include execution threads, open sockets, open files, locked mutexes, disk space and database connections.

C++’s model of resource management is based on the use of constructors and destructors to bind the life cycle of a resource that must be acquired to the lifetime of an object.

The C++ Standard Library containers all use RAII to implicitly manage resource acquisition and release.

The application of the Resource Acquisition Is Initialization (RAII) technique is fundamental to the idiomatic handling of resources in C++.

Standard library containers such as vector, map and string all manage their resources (such as memory, file handles and buffers) for you automatically.

The C++ Standard Library vector is an example of these techniques:

void f(const std::string& s)
{
std::vector<char> cv;
for (auto c : s){
cv.push_back(c);
std::cout  << c;
  }
}

The vector keeps its elements on the free store, but it handles all allocations and deallocations itself. In this example, push_back() automatically does new for you to acquire space for its elements and delete to free space that it no longer needs. However, when you use a std::vector (or any other Standard Library container), you don't need not know about or be concerned with the implementation details and can just rely on vector not leaking.

In practical terms, the main principle of RAII is to give ownership of any free store (heap)-allocated resource, for example, dynamically-allocated memory or system object handles to a stack-allocated object whose destructor contains the code to delete or free the resource and also any associated cleanup code.

Ownership

To avoid confusing pointers that must be deleted from pointers that must not, we introduce the concept of an owner. An owner is an object containing a pointer to an object allocated by new for which a delete is required.

An object constructed using new on the free store (dynamic memory, heap) must be destroyed using delete. Conversely, statically allocated objects (e.g., global variables), objects on the stack (local objects), and objects that are part of an enclosing object (class members) should never be explicitly deleted because they will be implicitly destroyed.

Every object on the free store (heap, dynamic store) must have exactly one owner. If there are two pointers to an object on the free store, only one can be the owner. An object not on the free store does not have an owner.

Objects that directly or indirectly hold an owner as an owner (e.g., a vector of pointers, a map, and a shared_ptr); are known as resource managers.

Modern C++ Memory Safety

Modern C++ avoids using Free Store (heap) memory as much as possible by declaring “owner” objects on the stack. When a resource is too large for the stack, and needs to be allocated on the Free Store then it should be owned by an object. As the object gets initialized, it acquires the resource it owns. The object is then responsible for releasing the resource in its destructor. The owning object itself is declared on the stack.

When a resource-owning stack object goes out of scope, its destructor is automatically invoked. For this reason, garbage collection (and its associated overhead) is unnecessary in C++, because object lifetime is deterministic. A resource can be always released at a known point in the program, which you can control.

#include <iostream>

class Entity
{
public:
    // Constructor 
    Entity(int value)
    {
    std::cout << "Created Entity with value " << value << ". \n";
    }

    // Destructor 
    ~Entity()
    {
    std::cout << "Destroyed Entity. \n";
    }
};
 
int main()
{
    // Free store (Heap) allocated 
    Entity* ef = new Entity(42);

    // Scope begins  
    {
    // Stack allocated   
    Entity e(43);
    }
    // End of scope for the stack allocated. Destructor is called.    

    delete ef; // Required for heap allocated without Smart pointer.

return 0;
}

The technique of acquiring resources in a constructor and releasing them in a destructor, known as Resource Acquisition Is Initialization or RAII, allows you to eliminate “naked new and delete operations”, that is, to avoid manual allocations in your code and instead have them automatically managed by well-behaved abstractions.

Avoiding naked new and naked delete makes code far less error-prone and far easier to keep free of resource leaks.

RAII guarantees that an acquired (initialized) resource is available to any function that may access the object. It also guarantees that all resources are released when the lifetime of their controlling object ends, in reverse order of acquisition. Likewise, if resource acquisition fails (the constructor exits with an exception), all resources acquired by every fully-constructed member and base subobject are released in reverse order of initialization. This leverages the core C++ language features (object lifetime, scope exit, order of initialization and stack unwinding) to eliminate resource leaks and guarantee exception safety.

For most resources (memory, locks, file handles, etc.), acquisition can fail, so a conventional resource manager must be able to report an error. The only fully general mechanism for that is throwing an exception if resource acquisition fails, and that’s what the standard library containers all do. This scheme handles nested objects, class hierarchies, and containers simply and efficiently.

Automatic resource management is all well and good for objects defined in a scope, releasing the resources they acquire at the exit from the scope, but what about objects allocated on the free store (heap)?

C++ Smart Pointers

In the <memory> header file, the C++ standard library provides two “smart pointers” to help manage objects on the free store:

  1. unique_ptr to represent unique ownership. Allows exactly one owner of the underlying pointer. Use as the default choice for C++ obects unless you know for certain that you require a shared_ptr

  2. shared_ptr to represent shared ownership. A shared_ptr is reference-counted smart pointer. Use when you want to assign one raw pointer to multiple owners. The raw pointer is not deleted until all shared_ptr owners have gone out of scope or have otherwise given up ownership.

    With smart pointers memory is managed through the standard C++ scoping rules so that the runtime environment is faster and more efficient.

A smart pointer is a class template that you declare on the stack, and initialize by using a raw pointer that points to a free store (heap)-allocated object. After the smart pointer is initialized, it owns the raw pointer. This means that the smart pointer takes care of deleting the memory that the raw pointer points to. The smart pointer destructor contains the call to delete, and because the smart pointer is declared on the stack, its destructor is invoked when the smart pointer goes out of scope, even if an exception is thrown somewhere further up the stack.

    #include <iostream>
    #include <memory>

    class Entity
    {
    public:
    // Constructor 
        Entity()
    {
        std::cout << "Created Entity. \n";
    }

    // Destructor 
    ~Entity()
        {
        std::cout << "Destroyed Entity. \n";
        }
    };

    int main()
    {
    // Smart Unique pointer. Calls Destructor at the end of scope. 
    std::unique_ptr<Entity> entity = std::make_unique<Entity>();

    // Smart Shared pointer gets destroyed when all references are gone.
    std::shared_ptr<Entity> shared_entity = std::make_shared<Entity>();

    return 0;
    }

Smart pointers are designed to be as efficient as possible both in terms of memory and performance.

When do you use “smart pointers”?

It's better to allocate objects on the stack, when you can. The stack is faster and safer because stack objects automatically get destroyed when they go out of scope.

When you really need the semantics of pointers, such as when a pointer to an object on the free store (heap) is needed, consider using the standard library types unique_ptr and shared_ptr to avoid resource leaks. For example, when your object needs to live longer than the current scope or when your object is large relative to the available stack size.

On Windows, the default stack size is 1MB, on Linux about 8 MB.

Using the /F number compiler command line option in Visual Studio you can set stack size. You can also set the stack size by using the /STACK linker option.

One major use of unique_ptr is to ensure proper deletion of objects allocated on the free store in a function.

unique_ptris a very lightweight mechanism with no space or time overhead compared to correct use of a raw built-in pointer. Its uses include passing free-store allocated objects in and out of functions.

When you need to share an object, you need pointers (or references) to refer to the shared object, so use a shared_ptr (unless there is an obvious single owner).

A reference is a restricted form of a pointer with some added syntactic sugar, so techniques for pointers also apply to references.

You do not need to use a pointer to return a collection of objects from a function; a C++ Standard Library container that is a resource handle will do that simply and efficiently.

Conclusion

C++ is a powerful programming language with outstanding performance. By using the built-in facilities of modern C++ and following a couple of simple rules, you can write safe, efficient C++ code, and avoid leaking resources.

Instead of using “naked” news and deletes, there are two general approaches to resource management that avoid such problems:

  1. [1]: Don’t put objects on the free store if you don’t have to; prefer scoped variables.

Wherever possible, have the resource manager object be a scoped variable. For scoped objects, acquisition is implicit at object construction (initialization) and destruction is implicit at scope exit.

  1. [2]: When you do construct an object on the free store, place its pointer into a manager object (sometimes called a handle) with a destructor that will destroy it. Examples are std::string, std::vector, all the other C++ Standard Library containers, and smart pointers(std::unique_ptr, std::shared_ptr).

Rule 2 is often referred to as RAII (Resource Acquisition Is Initialization) and is the basic technique for avoiding resource leaks and making error handling using exceptions simple and safe.

Additionally, many classical uses of the free store can be eliminated by using move semantics to return large objects represented as manager objects from functions.

References :

Learn more about the benefits of programming your applications with modern C++

Related services: C++ Software Development