Reputation: 88
Background:
In an effort to get better acquainted with memory management in C++, I recently decided to write my own memory management library.
Currently, this library doesn't consist of much other than a basic pool allocator, the entirety of which is small enough to inline below:
PoolAllocator::PoolAllocator (const AllocatorConfig& config)
{
this->nextAddress = 0;
this->size = config.AllocatorSize;
this->memoryArray = new byte[this->size];
}
PoolAllocator::~PoolAllocator ()
{
delete[] this->memoryArray;
}
void * PoolAllocator::Allocate (size_t size)
{
void* pointer = &(this->memoryArray[this->nextAddress]);
this->nextAddress += (unsigned long)size;
return pointer;
}
void PoolAllocator::Delete (void * object)
{
this->nextAddress -= sizeof (object);
}
I then use the allocator by replacing the global new to use the allocator (if it exists, otherwise it just uses malloc, which is what is called when the allocator itself is constructed).
The problem:
In testing this class, I wrote a simple application to allocate a std::string object, shown below:
#include <string>
#include <iostream>
#include "PoolAllocator.hpp"
#include "AllocatorConfig.hpp"
PoolAllocator *allocator;
int main (int numArgs, char *args[])
{
AllocatorConfig config = AllocatorConfig ();
config.AllocatorSize = 2048;
allocator = new PoolAllocator (config);
std::string *s = new std::string();
std::cout << "String object size: " << sizeof (*s) << std::endl;
*s = "test";
char *charBuffer = &((*s)[0]);
std::cout << "First allocate offset: " << (int)((int)&((*s)[0]) - (int)s) << std::endl;
std::cout << "Original cstring: " << charBuffer << std::endl;
*s = "thisIsALongStringToTestTheAddress";
std::cout << "Second allocate offset: " << (int)((int)&((*s)[0]) - (int)s) << std::endl;
std::cout << "Original cstring: " << charBuffer << std::endl;
delete allocator;
}
void * operator new(size_t size)
{
if (allocator)
{
return allocator->Allocate(size);
}
else
{
return malloc (size);
}
}
The output was as follows:
String object size: 28
First allocate offset: 4
Original cstring: test
Second allocate offset: 36
Original cstring: ,ÀH
This seemed fairly reasonable at first, given that the string primitive pointed to internally by the std::string needs to be contiguous, and std::string really has no way of knowing that it can simply expand into the adjacent contiguous space.
HOWEVER
When replacing both global delete
and delete[]
and attaching the debugger, I never saw any evidence that either was called. I also tried to break on free
, but that didn't work either, so...
The Question:
What is std::string doing with that memory? I would have assumed it would call delete[]
so my allocator could reclaim it, but as far as I can tell it's not releasing it at all. Is it just spraying the heap in that spot and holding on to it until it dies, or is there something else going on here that I'm missing?
Upvotes: 0
Views: 187
Reputation: 169028
What you're seeing is short string optimization where the string object stores very small strings directly in its own allocation, so there is nothing to deallocate on reassignment.
See this example, where we have a similar test and these results:
declaration
assignment of 'test'
assignment of 'thisIsALongStringToTestTheAddress'
allocate(34) = 0xb7ac30
assignment of 'thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress'
allocate(133) = 0xb7ac60
deallocate(0xb7ac30, 34)
scope end
deallocate(0xb7ac60, 133)
To interpret this:
"test"
there is no allocation.But wait, why don't you see any deallocation for the longer string that you assigned? Well, you never delete s;
and so your program terminates with a leaked allocation that the operating system will clean up itself. Add delete s;
prior to delete allocator;
and you should see the allocation freed in your operator delete
.
Source code of the test:
#include <iostream>
#include <string>
template <typename T, typename Allocator = std::allocator<T>>
class AllocatorProxy
{
private:
Allocator proxy;
public:
using pointer = typename Allocator::pointer;
using const_pointer = typename Allocator::const_pointer;
using value_type = typename Allocator::value_type;
using size_type = typename Allocator::size_type;
using difference_type = typename Allocator::difference_type;
pointer allocate(size_type n)
{
pointer p = proxy.allocate(n);
std::cout << "allocate(" << n << ") = " << static_cast<void *>(p) << '\n';
return p;
}
pointer allocate(size_type n, void const *l)
{
pointer p = proxy.allocate(n, l);
std::cout << "allocate(" << n << ", " << l << ") = " << static_cast<void *>(p) << '\n';
return p;
}
void deallocate(pointer p, size_type n) noexcept
{
std::cout << "deallocate(" << static_cast<void *>(p) << ", " << n << ")\n";
proxy.deallocate(p, n);
}
size_type max_size()
{
return proxy.max_size();
}
};
using astring = std::basic_string<char, std::char_traits<char>, AllocatorProxy<char>>;
int main() {
std::cout << "declaration\n";
astring str;
std::cout << "assignment of 'test'\n";
str.assign("test");
std::cout << "assignment of 'thisIsALongStringToTestTheAddress'\n";
str.assign("thisIsALongStringToTestTheAddress");
std::cout << "assignment of 'thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress'\n";
str.assign("thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress");
std::cout << "scope end\n";
return 0;
}
Upvotes: 3