Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 13

Placement New for Custom Memory Allocation

Recommended resources

Hello! Let's dive into our next lesson.

In our last session, we explored why specific data layouts are critical for performance by examining CPU caches, cache lines, and the principle of spatial locality. We concluded that arranging data contiguously is essential for building a fast system.

Today, we'll learn how to take manual control over that data layout. We'll dissect the standard C++ new operator and learn to use a powerful variant called placement new. This is the fundamental technique that will allow us to build the custom, high-performance memory allocators required for your telemetry engine. The learning outcome for this lesson is to allocate and deallocate raw memory using placement new for custom memory schemes.

The Two Steps of Object Creation

When you write MyObject* p = new MyObject();, the C++ runtime performs two distinct actions, usually hidden from you:

  1. Memory Allocation: It calls a function, operator new(sizeof(MyObject)), to request a block of raw, uninitialized memory from the operating system's heap manager.
  2. Object Construction: It invokes the MyObject constructor on that raw memory block, transforming it into a live object. The this pointer inside the constructor points to this memory block.

Similarly, delete p; is a two-step process in reverse:

  1. Object Destruction: It calls the object's destructor, p->~MyObject().
  2. Memory Deallocation: It calls operator delete(p) to return the memory to the heap manager.

Usually, we don't need to think about this separation. However, for high-performance systems, managing allocation ourselves is paramount. placement new is the C++ feature that allows us to separate these steps.


What is Placement New?

Placement new is a version of new that performs only the construction step. You provide it with a pointer to a pre-allocated block of memory, and it constructs an object at that exact location.

The C++ Standard Library FAQ provides a concise and authoritative explanation.

Destructors, C++ FAQ

The article 'Destructors, C++ FAQ' from isocpp.org explains the syntax and responsibilities of using placement new.

Please read the section titled 'What is “placement new” and why would I use it?'. Pay close attention to the syntax, the 'DANGER' section about your responsibilities (alignment and size), and how destruction is handled.

As the article emphasizes, when you use placement new, you take on several responsibilities:

  • You must pre-allocate the memory.
  • You must ensure the memory is large enough for the object you intend to create.
  • You must ensure the memory is correctly aligned for the object's type. (Recall our lesson on alignment).
  • You must explicitly call the destructor. You cannot use the standard delete operator, as it would try to deallocate memory that it did not allocate.

Here is a simple example demonstrating the full lifecycle:

#include <iostream>
#include <new> // Required for placement new

struct MyObject {
    int data;
    MyObject(int val) : data(val) {
        std::cout << "Constructor called! data = " << data << std::endl;
    }
    ~MyObject() {
        std::cout << "Destructor called! data = " << data << std::endl;
    }
};

int main() {
    // 1. Pre-allocate memory. Here, we use the stack.
    // Ensure it's large enough and aligned properly. std::aligned_storage is a
    // good helper for this, but a simple char array works for this example.
    alignas(MyObject) char buffer[sizeof(MyObject)];
    
    std::cout << "Memory allocated at address: " << (void*)buffer << std::endl;

    // 2. Construct the object in the pre-allocated memory using placement new.
    MyObject* obj = new (buffer) MyObject(42);

    std::cout << "Object constructed at address: " << obj << std::endl;
    std::cout << "Accessing object data: " << obj->data << std::endl;

    // 3. Explicitly call the destructor when done.
    obj->~MyObject();
    
    // The 'buffer' memory itself is automatically reclaimed when main() exits.
    // If we had used 'new char[]' to allocate it, we would need to call 'delete[]'.
    return 0;
}

Custom Memory Schemes: The Arena Allocator

So, why go through all this trouble? Placement new is the key to creating custom memory allocators, which are essential for the performance goals of your project. The most fundamental of these is the Arena Allocator (also known as a linear or bump allocator).

This article provides a fantastic, practical introduction to implementing an arena.

High Performance Memory Management: Arena Allocators

The article 'High Performance Memory Management: Arena Allocators' by 'sgn00' explains the concept and provides a C++ implementation.

Please read the following sections: 'What is an Arena?' to understand the core concept. The C++ code block for the Arena class to see its structure. The 'Example usage' section that follows, which shows how to combine the Arena class with placement new. Focus on how the allocate method works: it simply bumps a pointer and returns an address.

As you saw in the article, the arena pattern is beautifully simple and incredibly fast:

  1. Allocate Once: A single, large block of memory is requested from the OS at the start.
  2. Bump-Pointer Allocation: Subsequent "allocations" are just pointer arithmetic. There are no expensive system calls or complex data structures to manage. This makes allocations nearly free.
  3. Deallocate All at Once: Individual deallocations are not supported. The entire memory block is freed in one go, which is also extremely fast.

This pattern is a perfect fit for your telemetry engine. You can have a per-thread arena for profiling events. During a frame, you capture thousands of events by simply bumping a pointer in the arena, constructing event data with placement new. At the end of the frame (or when the buffer is full), the consumer thread processes the whole block, and you can then reset() the arena to be reused. This is the essence of high-frequency data capture.

Test your understanding!

Using the Arena class from the article you just read, write a short main function that does the following:

  1. Creates an Arena of 1024 bytes on the stack.
  2. Allocates memory from the arena for a MyObject struct (defined in the example above).
  3. Constructs a MyObject with the value 100 in that memory using placement new.
  4. Explicitly calls the object's destructor.

(Note: You don't need to free the arena's memory since it will be on the stack and cleaned up automatically).

Show answer
#include <iostream>
#include <new>
#include "Arena.h" // Assuming the Arena class from the article is in this header

struct MyObject {
    int data;
    MyObject(int val) : data(val) {
        std::cout << "Constructor called! data = " << data << std::endl;
    }
    ~MyObject() {
        std::cout << "Destructor called! data = " << data << std::endl;
    }
};

void use_arena() {
    // 1. Create an Arena of 1024 bytes.
    Arena arena(1024);
    
    std::cout << "Arena created." << std::endl;

    // 2. Allocate raw memory from the arena.
    void* raw_memory = arena.allocate(sizeof(MyObject), alignof(MyObject));
    std::cout << "Raw memory allocated from arena." << std::endl;

    // 3. Construct MyObject in the allocated memory.
    MyObject* obj = new (raw_memory) MyObject(100);

    // 4. Use the object...
    std::cout << "Object data: " << obj->data << std::endl;

    // 5. Explicitly call the destructor.
    obj->~MyObject();

    std::cout << "Arena is about to go out of scope, freeing all memory." << std::endl;
} // arena's destructor is called here, freeing the 1024-byte block.

int main() {
    use_arena();
    return 0;
}

Handling Complications: Destructors and Exceptions

The simple Arena is perfect for trivially destructible types (like simple structs of numbers), which will be common in your project. However, two important C++ correctness issues arise in the general case.

1. Non-Trivial Destructors: What if your object holds a resource, like a file handle or heap-allocated memory? If you just free the arena's buffer, the object's destructor is never called, and the resource leaks. The ObjArena shown in section 3 of the LINK article solves this by storing destructor function pointers within the arena itself, but this adds overhead. For maximum performance, it's best to design your event data to be trivially destructible.

2. Exceptions During Construction: This is a more subtle but critical issue. Consider this line:
MyObject* obj = new (raw_memory) MyObject(100);

What happens if the MyObject constructor throws an exception?

  • The memory at raw_memory has been allocated from our arena.
  • The object construction fails midway.
  • The program jumps to a catch block.
  • The memory from the arena is now leaked! It was allocated but never used, and the arena's offset has moved past it.

The C++ language has a mechanism to handle this automatically for robust custom allocators.

Destructors, C++ FAQ

The same C++ FAQ article explains how the compiler handles exceptions and how to plug this potential memory leak.

Read the section titled 'Is there a way to force new to allocate memory from a specific memory area?', focusing on the part labeled 'Problem #1: plugging the memory leak.' The key takeaway is how providing a matching operator delete overload gives the compiler the hook it needs to automatically clean up memory if a constructor throws.

By providing a corresponding operator delete for your placement operator new, you enable the compiler to generate exception-safe code. When a constructor throws, the compiler will automatically call your custom delete to deallocate the raw memory, preventing a leak. This is a crucial pattern for writing production-quality custom allocators.


Conclusion

You have now learned the fundamental mechanism for taking complete control over memory layout in C++. placement new is not a feature you'll use every day, but for systems programming, it is an indispensable tool. It empowers you to bypass the standard heap allocator and implement memory strategies tailored to your application's specific needs, unlocking a new level of performance.

Key Takeaways:

  • Standard new is a two-step process: memory allocation and object construction.
  • Placement new (new (ptr) Type(...)) performs only the construction step, placing an object into memory you provide.
  • Using placement new makes you responsible for allocation, alignment, size, and explicitly calling the destructor (ptr->~Type()).
  • This mechanism is the foundation for building high-performance custom memory allocators, such as the Arena (or bump) allocator.
  • Arenas offer extremely fast allocation/deallocation, making them ideal for managing large numbers of objects with similar lifetimes.
  • For robust allocators, you must consider how to handle non-trivial destructors and provide a matching operator delete to prevent memory leaks from constructor exceptions.

In our next lesson, we will build on this foundation to implement a simple linear allocator with bump pointer allocation. This will be the first practical, reusable memory management component of your high-performance telemetry engine.