CookMem 1.4.0
A C++ memory context / allocator
Loading...
Searching...
No Matches
Tutorials

CookMem divides the job of a memory context into three parts.

  1. The memory arena provides the API for allocating and deallocating large memory segments.
  2. The memory pool manages memory chunks within memory segments. This component is typically managed and hidden by the memory context.
  3. The logger monitors allocation and deallocation activity. Its primary use is to gather memory statistics.

Separating the memory arena from the memory pool makes it easy to build a custom memory context for your needs. See References for the available arenas and loggers. CookMem also provides ready-to-use memory contexts, including cookmem::SimpleMemContext and cookmem::CachedMemContext. This flexibility was a primary motivation for the project.

FixedArena

Often, we want allocations to be done within a large chunk of memory, such as a stack-based or pre-allocated memory buffer. cookmem::FixedArena is for this purpose.

/*
* Copyright (c) 2018-2026 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
// Reserve a 64,000-byte buffer for the arena to manage. The buffer lives
// on the stack, so its storage is limited and remains valid only until
// main returns. Keep it alive for at least as long as the arena and the
// memory context that use it.
char buffer[64000];
// FixedArena manages this caller-provided buffer; it does not allocate
// or own the buffer itself. CookMem requires 16-byte alignment, so if the
// buffer start is not aligned, FixedArena advances it to the next
// 16-byte boundary and reduces the usable size to account for the skipped
// prefix. It also rounds the remaining size down to a multiple of 16.
// Because the arena has a fixed capacity, allocations can fail when its
// available storage is exhausted.
cookmem::FixedArena arena (buffer, sizeof(buffer));
// NoActionMemLogger suppresses routine allocation and deallocation logs.
// It still reports detected memory errors by throwing an exception.
// MemContext combines the arena and logger and provides the allocation
// interface used below. It stores references to both, so they must remain
// alive for the lifetime of the context.
// Allocate at least 100 bytes. The returned pointer is null if the
// request cannot be satisfied by the fixed buffer.
void* ptr = memCtx.allocate (100);
// Return this block to the memory context so its space can be reused.
memCtx.deallocate (ptr);
// Like C's calloc, allocate one 100-byte block and initialize it to zero.
ptr = memCtx.callocate (1, 100);
// Grow the block to 1,000 bytes. Reallocation preserves its existing
// contents; if the request cannot be satisfied, it returns null and the
// original block remains valid.
ptr = memCtx.reallocate (ptr, 1000);
// Release the resized block when it is no longer needed.
memCtx.deallocate (ptr);
return 0;
}
Definition arena.h:79
Definition allocator.h:39
Definition logger.h:151

MmapArena

mmap is one way to obtain large segments of memory on Unix. cookmem::MmapArena calls mmap and munmap to obtain and release segments. On Windows, it uses VirtualAlloc and VirtualFree instead.

It should be noted that this arena does not track the memory segments since cookmem::MemPool already does the tracking.

/*
* Copyright (c) 2018-2021 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
// Use default arena
// allocate memory
void* ptr = memCtx.allocate (100);
// change the size of the memory
ptr = memCtx.reallocate (ptr, 1000);
// free the memory
memCtx.deallocate (ptr);
return 0;
}

CachedArena

cookmem::CachedArena caches released memory segments in a red-black tree and reuses them for later requests. This is useful when it is expensive to request and release memory segments.

Note that the memory pool does not release a segment to the arena as soon as the segment becomes free. It keeps free segments for later allocations, and releases them periodically, after a number of deallocations.

The arena keeps cached segments until it is destroyed or its releaseAll () is called, which returns them to the underlying arena. A context's releaseAllToSystem () releases its segments and then calls the arena's releaseAll ().

/*
* Copyright (c) 2018-2021 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
{
CachedMemCtx memCtx (cachedArena, logger);
// Allocate memory
void* ptr = memCtx.allocate (100);
// Change the size of the memory
ptr = memCtx.reallocate (ptr, 1000);
// Free the memory
memCtx.deallocate (ptr);
// Destructor of MemContext is called to release all allocated memory
// segments back to cachedArena
}
{
CachedMemCtx memCtx (cachedArena, logger);
// Allocate memory. The memory segment saved in cachedArena are re-used.
void* ptr = memCtx.allocate (100);
// Change the size of the memory
ptr = memCtx.reallocate (ptr, 1000);
// Free the memory
memCtx.deallocate (ptr);
// Destructor of MemContext is called to release all allocated memory
// segments back to cachedArena
}
return 0;
}
Definition mmaparena.h:115

Copying Data Between Memory Pools

cookmem can determine whether a memory pointer is in the address space of a memory pool. The check iterates through all memory segments owned by the pool.

This ability is quite useful to copy needed data out from the memory pool that is about to be destroyed.

/*
* Copyright (c) 2018-2021 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
// CachedMemContext is a child class of MemContext
// Create the parent ctx
ParentCachedMemCtx parentCtx;
int* ptrs[10];
// Allocate memory in the parent ctx
for (int i = 0; i < 5; ++i)
{
ptrs[i] = (int*)parentCtx.allocate (sizeof(int));
*ptrs[i] = i;
}
{
// Create the child ctx
//
// Note that the child shares the cachedArena with the parent.
ChildCachedMemCtx childCtx (parentCtx);
// Allocate memory in the child ctx
for (int i = 5; i < 10; ++i)
{
ptrs[i] = (int*)childCtx.allocate (sizeof(int));
*ptrs[i] = i;
}
// Before releasing the child ctx, copy things to parent ctx if necessary
for (int i = 0; i < 10; ++i)
{
if (childCtx.contains (ptrs[i]))
{
std::cout << "Copying index " << i << std::endl;
int* ptr = (int*)parentCtx.allocate (sizeof(int));
*ptr = *ptrs[i];
ptrs[i] = ptr;
}
}
// Child ctx is destroyed. No individual pieces of memories are
// deallocated. Instead, all the segments child ctx uses are simply
// released to the cachedArena, which can then be re-used by the
// parent ctx.
//
// This approach is very efficient if when you need to deal with
// lots of temporary small allocations that need to be deallocated
// at end of the task.
//
// At the same time, you can avoid having potentially large memory
// segments being allocated / deallocated frequently.
}
// Verify all memories are owned by parent ctx
for (int i = 0; i < 10; ++i)
{
if (!parentCtx.contains (ptrs[i]))
{
std::cout << "Oops: index " << i << " is problematic." << std::endl;
return 1;
}
}
return 0;
}

Querying Pointer Information

cookmem can report the allocated size of a pointer. By default, cookmem does not keep track of the exact allocated size. However, an option can be enabled to save the exact size. This option does not use extra memory, but it does require additional bookkeeping time.

Additionally, cookmem can determine whether a pointer is valid.

See the following example.

/*
* Copyright (c) 2021 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
void* ptr1 = memCtx.allocate (10);
// Expect output 16 because the alignment is 16
std::cout << "Allocated size for ptr1: " << memCtx.getSize (ptr1) << std::endl;
// Turning on storing the exact user size.
//
// It does not use extra memory. It just finds room in the existing memory
// to store the information.
memCtx.setStoringExactSize (true);
void* ptr2 = memCtx.allocate (10);
// Expect output 10
std::cout << "Allocated size for ptr2: " << memCtx.getSize (ptr2) << std::endl;
// Still expect output 16
std::cout << "Allocated size for ptr1: " << memCtx.getSize (ptr1) << std::endl;
// Expect 1 for both pointers.
//
// contains first make sure the memory is within memCtx segments,
// then it would check if the pointer is marked being used.
//
std::cout << "ptr1 is used: " << memCtx.contains (ptr1, true) << std::endl;
std::cout << "ptr2 is used: " << memCtx.contains (ptr2, true) << std::endl;
memCtx.deallocate (ptr1);
memCtx.deallocate (ptr2);
// Expect 0 for both pointers.
std::cout << "ptr1 is used: " << memCtx.contains (ptr1, true) << std::endl;
std::cout << "ptr2 is used: " << memCtx.contains (ptr2, true) << std::endl;
// Both pointers sizes are 0.
//
// If you want a perform a fast check if the pointer memory has been
// freed or not, like in debugger, use getSize ().
std::cout << "Size for ptr1: " << memCtx.getSize (ptr1) << std::endl;
std::cout << "Size for ptr2 " << memCtx.getSize (ptr2) << std::endl;
return 0;
}

Restricting Memory Usage

A memory context can limit the total size of the memory segments it obtains, which is its footprint. An allocation that would need memory beyond the limit fails and returns nullptr, while the context remains usable. This protects long-running programs from accidentally requesting a very large amount of memory.

  • setFootprintLimit () sets the limit in bytes. 0, the default, means no limit.
  • getFootprint () returns the current footprint, and getMaxFootprint () returns the largest footprint so far.

The footprint includes the overhead of the segments, so slightly less memory than the limit is available for allocations.

/*
* Copyright (c) 2026 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
// Limit the total size of the memory segments to 1 MB. 0, the default,
// means no limit.
memCtx.setFootprintLimit (1024 * 1024);
// A request that can never fit within the limit fails right away. The
// context is not affected and can still be used.
void* huge = memCtx.allocate (10 * 1024 * 1024);
std::cout << "10 MB request: " << (huge == nullptr ? "failed" : "succeeded") << std::endl;
// Allocate 100 KB blocks until the limit is reached.
const int maxBlocks = 100;
void* blocks[maxBlocks];
int numBlocks = 0;
while (numBlocks < maxBlocks)
{
void* ptr = memCtx.allocate (100 * 1024);
if (ptr == nullptr)
{
break;
}
blocks[numBlocks++] = ptr;
}
std::cout << "100 KB blocks allocated: " << numBlocks << std::endl;
std::cout << "Footprint: " << memCtx.getFootprint () << " bytes" << std::endl;
// Freeing memory makes room again.
memCtx.deallocate (blocks[--numBlocks]);
void* ptr = memCtx.allocate (100 * 1024);
std::cout << "After freeing one block, another 100 KB request: "
<< (ptr == nullptr ? "failed" : "succeeded") << std::endl;
memCtx.deallocate (ptr);
for (int i = 0; i < numBlocks; ++i)
{
memCtx.deallocate (blocks[i]);
}
// The largest footprint reached so far.
std::cout << "Maximum footprint: " << memCtx.getMaxFootprint () << " bytes" << std::endl;
return 0;
}

Memory Statistics

cookmem::StatsLogger gathers statistics on allocations and memory segments: counts, current and peak sizes, and how the allocations were satisfied. Use it as the logger of a memory context and read the statistics with getLogger ().getStats (). The default logger, cookmem::NoActionMemLogger, does nothing, so the statistics cost nothing unless you ask for them.

/*
* Copyright (c) 2026 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <string>
#include <vector>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
// Use StatsLogger instead of the default NoActionMemLogger to gather
// statistics on allocations and memory segments.
MemCtx memCtx;
std::vector<void*> ptrs;
for (int i = 0; i < 1000; ++i)
{
ptrs.push_back (memCtx.allocate (16 + i));
}
for (std::size_t i = 0; i < ptrs.size (); i += 2)
{
memCtx.deallocate (ptrs[i]);
}
const cookmem::MemStatistics& stats = memCtx.getLogger ().getStats ();
// Expect 1000 allocations and 500 deallocations.
std::cout << "Allocations: " << stats.getNumAllocations () << std::endl;
std::cout << "Deallocations: " << stats.getNumDeallocations () << std::endl;
// Sizes are the sizes requested by the user, not the chunk sizes.
std::cout << "Bytes currently allocated: " << stats.getCurrentAllocated () << std::endl;
std::cout << "Peak bytes allocated: " << stats.getPeakAllocated () << std::endl;
std::cout << "Memory segments in use: " << stats.getCurrentSegments ()
<< " (" << stats.getCurrentSegmentSize () << " bytes)" << std::endl;
// Print everything, including how the allocations were satisfied.
stats.printStats ();
for (std::size_t i = 1; i < ptrs.size (); i += 2)
{
memCtx.deallocate (ptrs[i]);
}
return 0;
}
Definition logger.h:202
std::size_t getCurrentSegmentSize() const
Definition logger.h:256
std::size_t getPeakAllocated() const
Definition logger.h:250
std::size_t getCurrentAllocated() const
Definition logger.h:248
std::size_t getNumDeallocations() const
Definition logger.h:232
std::size_t getCurrentSegments() const
Definition logger.h:252
std::size_t getNumAllocations() const
Definition logger.h:225
void printStats() const
Definition logger.h:287

Memory Corruption Detection

In addition to various assertions, cookmem can add padding bytes (between 1 and 8) after the end of the user-allocated memory. When a pointer is explicitly deallocated, a check determines whether the padding bytes were overwritten.

This feature can reduce allocation and deallocation performance. In some cases, it can also increase memory usage. However, it is a valuable debugging tool.

Note that once an exception is thrown, the memory context is in a bad state, so it is not a good idea to reuse it.

/*
* Copyright (c) 2021 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
try
{
// Set the number of padding bytes when creating a memory context.
//
// A value of 0 disables the check; values from 1 to 8 enable it.
std::cout << "Padding enabled: " << memCtx.isPadding () << std::endl;
void* ptr = memCtx.allocate (10);
// Expect output 10 because turning on padding also turning on
// the option to store the exact allocated size.
std::cout << "Allocated size for ptr: " << memCtx.getSize (ptr) << std::endl;
// Intentionally write pass the allocated size.
memset (ptr, 0xff, 11);
// Deallocation checks the configured padding byte.
// An exception is thrown in this function because it discovered
// that a padding byte has been modified.
memCtx.deallocate (ptr);
}
catch (cookmem::Exception& ex)
{
// Catch the exception and print the message.
std::cout << ex.getMessage () << std::endl;
}
return 0;
}
Definition exception.h:39
const char * getMessage() const
Definition exception.h:73

Caller-owned STL Allocator

Standard library containers accept custom allocators, allowing their storage to come from a CookMem memory context. cookmem::Allocator<T> adapts a context to the C++ allocator interface. For example, std::vector, std::list, and std::basic_string can allocate from the same context as the rest of the application.

With the default provider, the application creates and owns the context, then passes an allocator bound to it to each container. Containers using allocators bound to the same context can share its memory pool. This makes context ownership explicit and avoids global state, at the cost of passing allocator instances when constructing containers.

/*
* Copyright (c) 2025-2026 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <list>
#include <cookmem.h>
int
main ()
{
typedef cookmem::CachedMemContext<> MemoryContext;
typedef cookmem::Allocator<int> IntCallerAllocator;
// Create a memory context
MemoryContext context;
// Create an allocator for int type using the memory context
IntCallerAllocator allocator (context);
// Create a list of int using the custom allocator
std::list<int, IntCallerAllocator> values (allocator);
values.push_back (10);
values.push_back (20);
values.push_back (30);
for (std::list<int, IntCallerAllocator>::const_iterator it = values.begin ();
it != values.end ();
++it)
{
std::cout << *it << std::endl;
}
return 0;
}

Thread-local and Scoped Context STL Allocators

cookmem::TLSContextProvider provides one context per thread for each context type and tag type. Use a unique tag to keep allocator groups separate. By default, this context lasts for the lifetime of the thread. To limit it to a lexical scope instead, declare a cookmem::TLSContextScope before the containers. Default-constructed allocators created inside that scope use its context, which is destroyed when the scope exits.

Use TLSContextProvider<Context, Tag>::getActiveContext() to query the context currently used for allocations. When no matching scope is active, it returns the persistent thread-local context. TLSContextProvider<Context, Tag>::getThreadContext() returns the persistent thread-local context whether or not a scope is active. The provider instance's get() still returns the context to which that instance was bound.

The function-scope example uses TLSContextScope to release the context as soon as its containers are destroyed. It demonstrates both options: passing true makes its pool share the thread-local context's arena; with CachedMemContext, freed segments can be reused by either context. Passing false gives the scope its own arena. The thread-local example omits the scope, so the provider uses its per-thread context and releases it when the thread exits. In all cases, containers must be destroyed before their context ends and remain on the thread where their allocator was created. Neither approach requires passing allocator instances to containers.

To release a thread's memory before the thread exits, destroy its containers and call releaseAll () or releaseAllToSystem () on the thread-local context, which getThreadContext () returns. releaseAll () keeps the segments in the cached arena, ready for later allocations on the thread. releaseAllToSystem () also empties the cache, so the memory goes back to the system. The worker thread in the example uses releaseAllToSystem (), which matters when a thread could end without running thread-local destructors. Either way, every pointer allocated from the context becomes invalid, including those of containers that still use it.

/*
* Copyright (c) 2025-2026 Heng Yuan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <iostream>
#include <list>
#include <thread>
#include <cookmem.h>
// Here we create a unique class called MyContextTag.
// It does not have any other purpose than to be a unique type.
struct MyContextTag;
// The provider makes this context available to default-constructed allocators.
// Create an allocator for int type using the context provider.
// This allocator can be used to create STL containers without explicitly passing
// the allocator to the container constructor.
void
printValues (const std::list<int, MyAllocator>& values)
{
for (std::list<int, MyAllocator>::const_iterator it = values.begin ();
it != values.end ();
++it)
{
std::cout << *it << std::endl;
}
}
void
runWithThreadLocalContext ()
{
// Without a TLSContextScope, the provider uses one context per thread.
// The context releases its memory segments when this thread exits.
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
}
void
runWithSharedArena ()
{
// This scope supplies a local pool that shares the thread-local cached
// arena. Its free segments can be reused by other contexts on this thread.
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
}
void
runWithSeparateArena ()
{
// This scope supplies a local pool with its own arena.
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
}
void
runWorkerThread ()
{
{
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
} // Destroy the containers before releasing their memory.
// Return this thread's memory to the system now, instead of waiting for
// the thread-local context to be destroyed when the thread exits. Do
// this when the thread could end without running thread-local
// destructors. releaseAll () would instead keep the segments in the
// cached arena, ready for later allocations on this thread.
MyContextProvider::getThreadContext ().releaseAllToSystem ();
}
int
main ()
{
runWithThreadLocalContext ();
runWithSharedArena ();
runWithSeparateArena ();
std::thread worker (runWorkerThread);
worker.join ();
return 0;
}