Vector's push_back implementation using memcpy - c++

Given the following push_back code:
template <typename T>
void Vector<T>::push_back(const T& item) {
if (_size == _capacity) {
_capacity = _capacity + (_capacity > 1 ? (_capacity / 2) : 1);
T* newVec = new T[_capacity];
memcpy(newVec, _ptr, _size*(sizeof(T)));
delete [] _ptr;
_ptr = newVec;
}
_ptr[_size++] = item;
}
While the vector's class contain these members:
T* _ptr;
size_t _size;
size_t _capacity;
Is that implementation safe..? Will memcpy do his job correctly even if T is polymorphic type?
Would love to hear some suggestions about how to improve the implementation.

Don't use std::memcpy
You may only use std::memcpy on trivially copyable objects. Otherwise it's undefined behaviour.
You can, however, just copy all elements by hand. std::copy is suitable, as it may be specialized for trivial types:
In practice, implementations of std::copy avoid multiple assignments and use bulk copy functions such as std::memcpy if the value type is TriviallyCopyable
template <typename T>
void Vector<T>::push_back(const T& item) {
if (_size == _capacity) {
size_t new_cap = _capacity > 0 ? 2 * _capacity : 2;
T * newVec = new T[new_cap];
std::copy(_ptr, _ptr + _size, newVec);
std::swap(_capacity, new_cap);
std::swap(_ptr, newVec);
delete[] newVec;
}
_ptr[_size++] = item;
}
Note that your original implementation divided the capacity if the vector was too small.
More improvements
If you use std::allocator (or a compatible class), things get a little bit easier. You would use .allocate to get memory, .construct(pointer, value) to actually construct objects, .destroy to call their destructors and .deallocate to remove memory previously created with .allocate. Thus you don't need a default constructible object if you just want to use .push_back().
The following code is a quick minimal sketch. Note that there are some problems, for example reserve() isn't exception safe, as the allocated memory in tmp needs to be cleaned up if a constructor throws.
template <typename T, class Allocator = std::allocator<T> >
class Vector{
public:
typedef typename Allocator::pointer pointer;
typedef typename Allocator::size_type size_type;
Vector() : _ptr(0), _capacity(0), _size(0){}
~Vector() {
if(_capacity == 0)
return;
while(_size > 0)
pop_back();
_alloc.deallocate(_ptr, _capacity);
}
void reserve(size_type new_cap){
if(new_cap <= _capacity)
return;
// allocate memory
T * tmp = _alloc.allocate(new_cap);
// construct objects
for(unsigned int i = 0; i < _size; ++i){
_alloc.construct(tmp + i, _ptr[i]); // or std::move(_ptr[i])
}
// finished construction, save to delete old values
for(unsigned int i = 0; i < _size; ++i){
_alloc.destroy(_ptr + i);
}
// deallocate old memory
_alloc.deallocate(_ptr, _capacity);
_ptr = tmp;
_capacity = new_cap;
}
void push_back(const T& val){
if(_size == _capacity)
reserve(_capacity > 0 ? 2 * _capacity : 1);
_alloc.construct(_ptr + _size, val);
_size++; // since T::T(..) might throw
}
void pop_back(){
_alloc.destroy(_ptr + _size - 1);
_size--;
}
T& operator[](size_type index){
return _ptr[index];
}
private:
pointer _ptr;
size_type _capacity;
size_type _size;
Allocator _alloc;
};

This is not safe, for example if T is doing this:
struct T
{
T* myself;
T() : myself(this) {}
void foo() { myself->bar(); }
void bar() { ... }
};
Since you moved the memory location of the object by simply moving it's memory without calling constructors/destructors, myself will not be updated and when you call foo afterward, it will call bar with an invalid this pointer.

Imagine what would happen if T was itself a vector.
Now you have two vectors pointing to the same buffer, and they will both delete the buffer... bad idea.
(Well, technically it's undefined behavior the moment you memcpy. I just gave you the most likely result.)

It's not safe in general - but C++11 provides std::is_trivially_copyable :
#include <type_traits>
...
if (std::is_trivially_copyable<T>::value)
// *can* use memcpy...

Related

Custom allocator for nested containers

I am working on custom allocators. So far, I have tried to work on simple containers: std::list, std::vector, std::basic_string, etc...
My custom allocator is a static buffer allocator, its implementation is straightforward:
#include <memory>
template <typename T>
class StaticBufferAlloc : std::allocator<T>
{
private:
T *memory_ptr;
std::size_t memory_size;
public:
typedef std::size_t size_type;
typedef T *pointer;
typedef T value_type;
StaticBufferAlloc(T *memory_ptr, size_type memory_size) : memory_ptr(memory_ptr), memory_size(memory_size) {}
StaticBufferAlloc(const StaticBufferAlloc &other) throw() : memory_ptr(other.memory_ptr), memory_size(other.memory_size){};
pointer allocate(size_type n, const void *hint = 0) { return memory_ptr; } // when allocate return the buffer
void deallocate(T *ptr, size_type n) {} // empty cause the deallocation is buffer creator's responsability
size_type max_size() const { return memory_size; }
};
I am using it in this fashion:
using inner = std::vector<int, StaticBufferAlloc<int>>;
int buffer[201];
auto alloc1 = StaticBufferAlloc<int>(&buffer[100], 50);
inner v1(0, alloc1);
assert(v1.size() == 0);
const int N = 10;
// insert 10 integers
for (size_t i = 0; i < N; i++) {
v1.push_back(i);
}
assert(v1.size() == N);
All good so far, when I grow N past the max buffer size it throws and that's expected.
Now, I am trying to work with nested containers. In short, am trying to have a vector of the vector (matrix), where the parent vector and all its underlying elements (that are vectors i.e. containers) share the same static buffer for allocation. It looks like scoped_allocator can be a solution for my problem.
using inner = std::vector<int, StaticBufferAlloc<int>>;
using outer = std::vector<inner, std::scoped_allocator_adaptor<StaticBufferAlloc<inner>>>;
int buffer[201];
auto alloc1 = StaticBufferAlloc<int>(&buffer[100], 50);
auto alloc2 = StaticBufferAlloc<int>(&buffer[150], 50);
inner v1(0, alloc1);
inner v2(0, alloc2);
assert(v1.size() == 0);
assert(v2.size() == 0);
const int N = 10;
// insert 10 integers
for (size_t i = 0; i < N; i++)
{
v1.push_back(i);
v2.push_back(i);
}
assert(v1.size() == N);
assert(v2.size() == N);
outer v // <- how to construct this vector with the outer buffer?
v.push_back(v1);
v.push_back(v2);
...
My question is how to initialize the outer vector on its constructor call with its static buffer?
Creating a scoped allocator in C++11/C++14 was a little bit challenging. So I opted for a very modern solution introduced in C++17. Instead of implementing an allocator, I used polymorphic_allocator. Polymorphic allocators are scoped allocators, standard containers will automatically pass the allocators to sub-objects.
Basically, the idea was to use a polymorphic allocator and inject it with monotonic_buffer_resource. The monotonic_buffer_resource can be initialized with a memory resource.
Writing a custom memory resource was very simple:
class custom_resource : public std::pmr::memory_resource
{
public:
explicit custom_resource(std::pmr::memory_resource *up = std::pmr::get_default_resource())
: _upstream{up}
{
}
void *do_allocate(size_t bytes, size_t alignment) override
{
return _upstream; //do nothing, don't grow just return ptr
}
void do_deallocate(void *ptr, size_t bytes, size_t alignment) override
{
//do nothing, don't deallocate
}
bool do_is_equal(const std::pmr::memory_resource &other) const noexcept override
{
return this == &other;
}
private:
std::pmr::memory_resource *_upstream;
};
Using it is even simpler:
std::byte buffer[512];
custom_resource resource;
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer), &resource};
std::pmr::vector<std::pmr::vector<int>> outer(&pool)
It is important to note that std::pmr::vector<T> is just std::vector<T, polymorphic_allocator>.
Useful resources:
CppCon 2017: Pablo Halpern “Allocators: The Good Parts”
C++ Weekly - Ep 222 - 3.5x Faster Standard Containers With PMR
Purpose of scoped allocator
std::pmr is cool but it requires modern versions of gcc to run (9+). Fortunately, Reddit is full of kind strangers. A C++14 solution can be found here.

I can't understand how to solve problem with memory leak C++

Condition
In lectures, we have already started to implement our vector. In this task, you need to develop it: add the Size, Capacity, and PushBack methods. Send the simple_vector.h header file containing the SimpleVector class template declaration and definition for verification:
Requirements:
the Capacity method should return the current capacity of the vector — the number of elements that fit into the memory block currently allocated by the vector
the Size method must return the number of elements in the vector
the PushBack method adds a new element to the end of the vector; if there is no free space left in the current allocated memory block (i.e. Size() == Capacity()), the vector must allocate a block of size 2 * Capacity(), copy all the elements to it, and delete the old one.
the first call to the PushBack method for a newly created object must make the capacity equal to one
the Push Back method must have a amortized constant complexity
the begin and end methods must return iterators the current beginning and end of the vector
the current memory block allocated by the vector must be freed in the destructor
also see the attached solution template for additional requirements for working with SimpleVector in unit tests.
The preparation of the decision:
simple_vector.h: https://d3c33hcgiwev3.cloudfront.net/q-OL4qX_EeilzRLZf2WxfA_ac4e8270a5ff11e89fd0455a8819d387_simple_vector.h?Expires=1596067200&Signature=cLfBpytTripoqpOYaW9g4~2-JqTI~8HtxahNwNATwBeq28RdXCvkcqghN~UUPv~wx1XZTVOTs8JDsZQjEALk6Soy70QFADkK9lSfFpLNcQq-Dxd4oxk-C5QDEhadM1LrVGe8Rmz0jRYgIV5sDTvAATBhiY3k-KqbAaDe1AK6QiE_&Key-Pair-Id=APKAJLTNE6QMUY6HBC5A
simple_vector.cpp: https://d3c33hcgiwev3.cloudfront.net/uoPvEoauEeianAr0yIdmDg_bae6cec086ae11e88d9327752d64e780_simple_vector.cpp?Expires=1596067200&Signature=CE1Mox1yU6LjGDXL1xstxT9anv9NI~otNwhBw5AbPyLBquRIi9E6cotR~BQsrvU-djoksfjV9YgnsyF00eFnVjsk~oF0z18wkVkgdIirPB-NNLH0aFvD4WFG97qmSuD0WjeetWyi6UR5BKYCnwfO~ax6-HZLM-GWheO9LHc~BvE_&Key-Pair-Id=APKAJLTNE6QMUY6HBC5A
Comment:
The header file that you send for verification should not include the <vector>, <list>, <forward_list>, <deque>, <map> files. If you have one of these files enabled, you will get a compilation error.
Hint:
For sure, your implementation of the SimpleVector class template will have a field that is a pointer. In the default constructor, you will need to initialize it with something. In the lectures, we only discussed one way to initialize pointers — using the new operator. In C++, there is a special value that means a pointer that points to nothing — nullptr:
int* p = nullptr;
string* q = nullptr;
map<string, vector<int>>* r = nullptr;
You can use nullptr to initialize the pointer in the default constructor.
How to send:
When the work is ready, you can upload files for each part of the task on the 'My work'tab.
And here is my .hsolution to which the Coursera testing system responds a 10 != 8: Memory leak detected. However I can't figure out where the leak is going. Help me pls.
#pragma once
#include <cstdlib>
using namespace std;
template <typename T>
class SimpleVector {
public:
SimpleVector()
: data(nullptr)
, end_(data)
, size_(0) {}
explicit SimpleVector(size_t size)
: data(new T[size])
, end_(data + size)
, size_(size) {}
~SimpleVector() {
delete[] data;
}
T& operator[](size_t index) { return data[index]; }
T* begin() const { return data; }
T* end() const { return end_; }
size_t Capacity() const { return end_ - data; }
size_t Size() const { return size_; }
void PushBack(const T& value) {
if (size_ == Capacity()) {
if (size_ == 0) {
delete[] data;
data = new T[1];
data[size_] = value;
++size_;
end_ = data + size_;
}
else {
T* local_data = new T[size_];
for (size_t i = 0; i < size_; ++i) {
local_data[i] = data[i];
}
delete[] data;
data = new T[2 * Capacity()];
for (size_t i =0; i < size_; ++i) {
data[i] = local_data[i];
}
delete[] local_data;
data[size_] = value;
++size_;
end_ = data + size_ * 2;
}
}
else {
data[size_] = value;
size_++;
}
}
private:
T *data;
T *end_;
size_t size_;
};
Thank you in advance.
There is a memory leak in PushBack due to lack of exception safety. Consider:
T* local_data = new T[size_];
// potentially throwing operations here...
delete[] local_data;
If those operations throw, then delete[] local_data; will never be executed.
Typical way to avoid such memory leak is to use smart pointers instead of bare pointers for ownership. The antiquated way is to use try-catch.
Your class also fails to enforce the class invariant of uniqueness of data pointer. Such constraint is essential for the destructor to be correct, because an allocation must be deleted exactly once, and no more.
Making a copy of an instance of the class will result in undefined behaviour because of same pointer being deleted in multiple destructors. Another consequence is that the assigment operators will leak the previously allocated memory (before the UB occurs in the destructor):
{
SimpleVector vec(42);
SimpleVector another(1337);
SimpleVector vec = another; // memory leak in assignment operator
} // undefined behaviour in the destructor
The problem is in the copy and move constructors and assignment operators, which you've left as implicitly generated. The implicitly generated special member functions will copy the pointer value, violating its uniqueness (and failing to delete the previous allocation in case of assignment). In other words, those functions perform a shallow copy.
Using a smart pointer as the member is an easy solution. Otherwise, you must implement copy and move constructors and assignment operators that don't leak, nor violate uniqueness.
Note that even if you did use a smart pointer, you'd still need user defined copy etc. because of the end pointer. If you instead used an integer that is relative to data, then you could avoid defining those functions.
P.S. There is no need to allocate twice, and copy twice. Instead, allocate one larger buffer, copy the old one, delete the old, point to the new.
P.P.S. As a sidenote: The vector you are implementing behaves quite differently from the standard vector, which is probably intentional by your teacher. When I add an object to a vector of 10 elements, I would expect only one element to be created and possibly 10 be copied due to relocation, rather than 20 objects being created with 9 being unaccessible.
A proper implementation of vector separates the allocation of memory, and creation of objects into that memory which allows the growth of the memory to be geometric without creating objects until they are added into the vector. I suspect that how to do this is outside the scope of your exercise.
I wouldn't call it a leak, but you treat end_ inconsistently. It seems like you are treating Size and Capacity as equivalent values, they are not.
Either end_ should point one past the allocated (but not necessarily populated) memory, and you return data + size in end(), or it should point one past the last element, and you should store size_t capacity_ not size_t size_;
Here is solution without memory leak. Thank you.
#pragma once
#include <cstdlib>
using namespace std;
template <typename T>
class SimpleVector {
public:
SimpleVector() {
data_ = nullptr;
end_ = data_;
size_ = 0;
capacity_ = 0;
}
explicit SimpleVector(size_t size) {
data_ = new T[size];
end_ = data_ + size;
size_ = size;
capacity_ = size;
}
SimpleVector(const SimpleVector& that)
: data_(that.data_)
, end_(that.end_)
, size_(that.size_)
, capacity_(that.capacity_) {}
SimpleVector& operator = (const SimpleVector& that) {
data_ = that.data_;
end_ = that.end_;
size_ = that.size_;
capacity_ = that.capacity_;
}
~SimpleVector() { delete[] data_; }
T& operator[](size_t index) {
return data_[index];
}
T* begin() const { return data_; }
T* end() const { return data_ + size_; }
size_t Capacity() const { return capacity_; }
size_t Size() const { return size_; }
void PushBack(const T& value) {
if (size_ == capacity_) {
if (capacity_ == 0) { // т. е. создали конструктором по умолчанию, size_ = 0
data_ = new T[1];
capacity_ = 1;
data_[size_] = value;
++size_;
end_ = data_ + size_;
}
else if (capacity_ == size_) { // т. е. capacity_ == size_
T* local_data = new T[2 * size_];
for (size_t i = 0; i < size_; ++i) {
local_data[i] = data_[i];
}
delete[] data_;
data_ = new T[2 * size_];
for (size_t i = 0; i < size_; ++i) {
data_[i] = local_data[i];
}
delete[] local_data;
data_[size_] = value;
size_++;
capacity_ *= 2;
end_ = data_ + size_;
}
}
else {
data_[size_] = value;
size_++;
}
}
private:
T *data_;
T *end_;
size_t size_;
size_t capacity_;
};

How to implement erase on vector in c++

Learning from Accelerated C++: Practical Programming by Example, in chapter 11, there was an implementation (only with basic features) of vector container from STL. After that was an exercise for implementing erase function just as std::vector does. What I have tried:
#include <memory>
template<class T>
class Vec{
private:
T *data;
T *avail;
T *limit;
std::allocator<T> alloc;
...
public:
explicit Vec(size_t n, const T &val = T())
{
create(n, val);
}
T *const begin()
{
return data;
}
T *const end()
{
return avail;
}
T *erase(T* const pos);
...
};
template <class T>
void Vec<T>::create(size_t n, const T &val)
{
data = alloc.allocate(n);
limit = avail = data + n;
std::uninitialized_fill(data, limit, val);
}
// here I am trying to implement the erase function with 3 pointers (data, avail, limit)
template<class T>
T* Vec<T>::erase(T *const i)
{
if(i==end())
{
return end();
}
else if(i >= begin() && i < end())
{
size_t member = i-data;
size_t size = limit-data;
T* new_data = alloc.allocate(size);
std::uninitialized_copy(data, i, new_data);
T* new_avail = std::uninitialized_copy(i+1, avail, i);
data = new_data;
avail = new_avail;
limit = data + size;
return &data[member];
}
else
{
return 0;
}
}
Now If I want to check, if that function works correctly:
#include "vec.hpp"
int main()
{
Vec<int> v(5, 2);
if (v.erase(v.begin()+2))
{
for (int i:v)
{
cout << i << endl;
}
}
}
I get
...
0
0
0
0
Segmentation fault
I have somehow made infinity allocation-loop, but I have no idea how. Anyway, How can I make the erase function (or in another words, how to shift elements after the erased one to left), via std::uninitialized_copy?
EDIT:
the whole class definition is there:
https://www.codepile.net/pile/rLmz8wRq
Here you create new storage for vector:
T* new_data = alloc.allocate(size);
and here you write to array pointed by argument, which (supposedly) points to location on old storage. new_avail would point to old storage.
T* new_avail = std::uninitialized_copy(i+1, avail, i);
^ that's destination
^ that's source
Then you even leak memory:
data = new_data; // old storage pointed by data is lost along with the "tail" of array
After this vector state is completely broken, pointer arithmetic go to undefined area:
avail = new_avail; // avail points to old storage, data points to new one.
// (data < avail) no longer guaranteed to be true
Because in all likelihood data would be greater than avail, you don't get an infinite loop, you may get a very long one. OR may not. Attempt to iterate through vector after this "erase" amounts to Undefined Behavior.

auto_ptr with an array of POD and uninitialized values

This is very similar to auto_ptr for arrays. However, my wrinkle is I don't want an initialized array, which is what a vector would provide (the const T& value = T()):
explicit vector(size_type count,
const T& value = T(),
const Allocator& alloc = Allocator());
I don't want the array initialized because its a large array and the values will be immediately discarded.
I'm currently hacking it with the following, but it feels like something is wrong with it:
//! deletes an array on the heap.
template <class T>
class AutoCleanup
{
public:
AutoCleanup(T*& ptr) : m_ptr(ptr) { }
~AutoCleanup() { if (m_ptr) { delete[] m_ptr; m_ptr = NULL; }}
private:
T*& m_ptr;
};
And:
// AutoCleanup due to Enterprise Analysis finding on the stack based array.
byte* plaintext = new byte[20480];
AutoCleanup<byte> cleanup(plaintext);
// Do something that could throw...
What does C++ provide for an array of a POD type that is uninitialized and properly deleted?
The project is C++03, and it has no external dependencies, like Boost.
From your question, it's not easy to interpret what you actually need. But I guess you want an array which is stack-guarded (i.e. lifetime bound to the stack), heap-allocated, uninitialized, dynamically or statically sized, and compatible with pre-C++11 compilers.
auto_ptr can't handle arrays (because it doesn't call delete[] in its destructor, but rather delete).
Instead, I would use boost::scoped_array together with new[] (which doesn't initialize POD types afaik).
boost::scoped_array<MyPodType> a(new MyPodType[20480]);
If you don't want to use boost, you can reimplement scoped_array pretty easily by pasting the code together which this class includes from the boost library:
#include <cassert>
#include <cstddef>
// From <boost/checked_delete.hpp>:
template<class T>
inline void checked_array_delete(T * x)
{
typedef char type_must_be_complete[sizeof(T) ? 1 : -1];
(void) sizeof(type_must_be_complete);
delete [] x;
}
// From <boost/smartptr/scoped_array.hpp>:
template<class T>
class scoped_array
{
private:
T * px;
// Make this smart pointer non-copyable
scoped_array(scoped_array const &);
scoped_array & operator=(scoped_array const &);
typedef scoped_array<T> this_type;
public:
typedef T element_type;
explicit scoped_array(T * p = 0) : px(p) { }
~scoped_array() {
checked_array_delete(px);
}
void reset(T * p = 0) {
assert(p == 0 || p != px); // catch self-reset errors
this_type(p).swap(*this);
}
T & operator[](std::ptrdiff_t i) const {
assert(px != 0);
assert(i >= 0);
return px[i];
}
T * get() const {
return px;
}
operator bool () const {
return px != 0;
}
bool operator ! () const {
return px == 0;
}
void swap(scoped_array & b) {
T * tmp = b.px;
b.px = px;
px = tmp;
}
};

Memory allocation of internal types used by the containers

C++11 standard has following lines in General Container Requirements.
(23.2.1 - 3)
For the components affected by this subclause that declare an allocator_type, objects stored in these components shall be constructed using the allocator_traits::construct function and destroyed using the allocator_traits::destroy function (20.6.8.2). These functions are called only for the container’s element type, not for internal types used by the container
(23.2.1 - 7)
Unless otherwise specified, all containers defined in this clause obtain memory using an allocator
Is it true or not, that all memory used by container is allocated by specified allocator? Because standard says that internal types are constructed not with allocator_traits::construct, so there should be some kind of call to operator new. But standard also says that all containers defined in this clause obtain memory using an allocator, which in my opinion means that it can't be ordinary new operator, it has to be placement new operator. Am I correct?
Let me show you example, why this is important.
Let's say we have a class, which holds some allocated memory:
#include <unordered_map>
#include <iostream>
#include <cstdint>
#include <limits>
#include <memory>
#include <new>
class Arena
{
public:
Arena(std::size_t size)
{
size_ = size;
location_ = 0;
data_ = nullptr;
if(size_ > 0)
data_ = new(std::nothrow) uint8_t[size_];
}
Arena(const Arena& other) = delete;
~Arena()
{
if(data_ != nullptr)
delete[] data_;
}
Arena& operator =(const Arena& arena) = delete;
uint8_t* allocate(std::size_t size)
{
if(data_ == nullptr)
throw std::bad_alloc();
if((location_ + size) >= size_)
throw std::bad_alloc();
uint8_t* result = &data_[location_];
location_ += size;
return result;
}
void clear()
{
location_ = 0;
}
std::size_t getNumBytesUsed() const
{
return location_;
}
private:
uint8_t* data_;
std::size_t location_, size_;
};
we also have custom allocator:
template <class T> class FastAllocator
{
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
template <class U> class rebind
{
public:
typedef FastAllocator<U> other;
};
Arena* arena;
FastAllocator(Arena& arena_): arena(&arena_) {}
FastAllocator(const FastAllocator& other): arena(other.arena) {}
template <class U> FastAllocator(const FastAllocator<U>& other): arena(other.arena) {}
//------------------------------------------------------------------------------------
pointer allocate(size_type n, std::allocator<void>::const_pointer)
{
return allocate(n);
}
pointer allocate(size_type n)
{
return reinterpret_cast<pointer>(arena->allocate(n * sizeof(T)));
}
//------------------------------------------------------------------------------------
void deallocate(pointer, size_type) {}
//------------------------------------------------------------------------------------
size_type max_size() const
{
return std::numeric_limits<size_type>::max();
}
//------------------------------------------------------------------------------------
void construct(pointer p, const_reference val)
{
::new(static_cast<void*>(p)) T(val);
}
template <class U> void destroy(U* p)
{
p->~U();
}
};
This is how we use it:
typedef std::unordered_map<uint32_t, uint32_t, std::hash<uint32_t>, std::equal_to<uint32_t>,
FastAllocator<std::pair<uint32_t, uint32_t>>> FastUnorderedMap;
int main()
{
// Allocate memory in arena
Arena arena(1024 * 1024 * 50);
FastAllocator<uint32_t> allocator(arena);
FastAllocator<std::pair<uint32_t, uint32_t>> pairAllocator(arena);
FastAllocator<FastUnorderedMap> unorderedMapAllocator(arena);
FastUnorderedMap* fastUnorderedMap = nullptr;
try
{
// allocate memory for unordered map
fastUnorderedMap = unorderedMapAllocator.allocate(1);
// construct unordered map
fastUnorderedMap =
new(reinterpret_cast<void*>(fastUnorderedMap)) FastUnorderedMap
(
0,
std::hash<uint32_t>(),
std::equal_to<uint32_t>(),
pairAllocator
);
// insert something
for(uint32_t i = 0; i < 1000000; ++i)
fastUnorderedMap->insert(std::make_pair(i, i));
}
catch(std::bad_alloc badAlloc)
{
std::cout << "--- BAD ALLOC HAPPENED DURING FAST UNORDERED MAP INSERTION ---" << std::endl;
}
// no destructor of unordered map is called!!!!
return 0;
}
As you can see, destructor of unordered_map is never called, but memory is freed during destruction of arena object. Will there be any memory leak and why?
I would really appreciate any help on this topic.
An allocator is supposed to provide 4 functions (of interest here):
2 are used for memory management: allocate/deallocate
2 are used for objects lifetime management: construct/destroy
The these functions in your quote only apply to construct and destroy (which were mentioned in the previous sentence), and not to allocate/deallocate, thus there is no contradiction.
Now, regarding memory leaks, for an arena allocator to work not only should the objects in the container be built using the arena allocator (which the container guarantees) but all the memory those objects allocate should also be obtained from this allocator; this can get slightly more complicated unfortunately.