Aside from the word vector being a misnomer, I have issues with both std::vector and std::string, especially in beginner programmers' uses.
Recently, I've spent almost three weeks of nothing but memory management hell basically because people misused this seemingly harmless container.
Let's take an example:
1 2 3 4 5 6 7
|
int main()
{
std::vector<int> myVector;
myVector.push_back(1);
myVector.push_back(2);
}
|
Harmless right? Wrong. The user here knows they needed only two integers. Because of how a vector works, instead of making one large chunk of memory with enough space for two integers, it instead allocates a small block of memory big enough for only one, then reallocates a second chunk of memory that no longer fits in the first block of memory, potentially causing fragmentation. Let's look into how this works exactly by implementing a dummy allocator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
#include <vector>
#include <iostream>
template <typename T>
class DummyAllocator {
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
/* Because this makes sense... */
template <class U>
struct rebind {
typedef DummyAllocator<U> other;
};
pointer allocate(std::size_t num, const void* = 0)
{
std::cout << "Allocating " << num <<
" of type " << typeid(T).name() << "\n";
return new T[num];
}
void deallocate(pointer p, std::size_t num)
{
std::cout << "Deallocating " << num <<
" of type " << typeid(T).name() << "\n";
delete [] p;
}
void construct(pointer p, const_reference value) {
new ((void*)p) T(value);
}
void destroy(pointer p) {
p->~T();
}
};
int main()
{
std::vector<int, DummyAllocator<int> > myVector;
myVector.push_back(1);
myVector.push_back(2);
}
|
Allocating 1 of type i
Allocating 2 of type i
Deallocating 1 of type i
Deallocating 2 of type i |
So, why do people do this? Because it's convenient for the user, despite it being such a ridiculous thing to do. Picture this being done on a scale of kilobytes or megabytes... and you have tons of fragmentation that isn't particularly easy to fix.
I will at least say that std::vector gives you the power to prevent this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
#include <vector>
#include <iostream>
template <typename T>
class DummyAllocator {
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
/* Because this makes sense... */
template <class U>
struct rebind {
typedef DummyAllocator<U> other;
};
pointer allocate(std::size_t num, const void* = 0)
{
std::cout << "Allocating " << num <<
" of type " << typeid(T).name() << "\n";
return new T[num];
}
void deallocate(pointer p, std::size_t num)
{
std::cout << "Deallocating " << num <<
" of type " << typeid(T).name() << "\n";
delete [] p;
}
void construct(pointer p, const_reference value) {
new ((void*)p) T(value);
}
void destroy(pointer p) {
p->~T();
}
};
int main()
{
std::vector<int, DummyAllocator<int> > myVector;
myVector.reserve(2);
myVector.push_back(1);
myVector.push_back(2);
}
|
Allocating 2 of type i
Deallocating 2 of type i |
I sadly do not see this being done... ever. Rather, in most cases, a linked list should have been where you see a lot of push_back calls which is more forgiving with fragmentation as it doesn't require a contiguous amount of memory and is generally faster with inserting objects.
std::string is the same way. If you have a string that simply keeps expanding, you *must* remember to reserve memory for it. You do not want to keep reallocating memory from smaller to bigger. That's a hack at best in desperate situations where you know you're taking a loss. If you do this in games or large server applications, you're going to be looking at several hundred megabytes (possibly gigabytes) of memory lost to fragmentation
Note that most of this is directed towards larger applications that allocate more than just a few megabytes on the heap and further discussion should probably be on how to avoid fragmentation. Still, it's generally good practice and there's little effort into doing it right *the first time*. Please... just use .reserve() or don't use a std::vector or std::string at all.