The C++11 standard added unique_ptr which goal is to replace and correct the weaknesses of the deprecated auto_ptr.
This post will focus on the memory payload of the unique_ptr with a custom deleter.
Let’s consider the output of the program compiled with g++4.7 on a linux 32bits:
1 2 3 4 5 6 |
sizeof(std::unique_ptr<double>): 4 sizeof(std::unique_ptr<double, void(*)(double*)>): 8 sizeof(std::unique_ptr<double, std::function<void (double*)>>): 20 sizeof(std::unique_ptr<double, DoubleEmptyDeleter>): 4 sizeof(std::unique_ptr<double, GenericDeleter<double, &erase>>): 4 sizeof(UniquePtr<double, &erase>): 4 |
- As expected, the size of unique_ptr with default deleter is the same as a raw pointer (namely 4 bytes).
- More surprisingly, the size of unique_ptr with a simple function pointer is larger than unique_ptr with default deleter (8 bytes).
This is normal because the unique_ptr must store the address of the deleter function as well. Thus, the size of unique_ptr is the size of the raw pointer plus the size of the deleter (plus some possible extra depending on the alignment).
But this is also unfortunate, because this payload overhead can have a big impact on the memory usage, especially when largely spreading this practice.
The real issue resides in the fact that we pay for an extra runtime degree of flexibility that we usually do not need: we could indeed specify at runtime different deleter functions for the same allocated type. Even if attractive, this is usually uncommon.
Now the question that raises is: could it be possible to have a unique_ptr with customized deleter with the same payload as a raw pointer? After all, there is no payload overhead when invoking explicitly factory balanced functions “create” and “destroy”. So it would be great if we could get the best of both worlds. The good news is that this is possible. - The third result is more alarming. The usage of a general “std::function” is pricey. This is due to the size of std::function itself. Here again, it offers a high runtime flexibility that is usually not needed.
- So how could we customize the deleter of unique_ptr having the size of the the raw pointer?
Simply, by using an empty deleter object and at the condition that unique_ptr<> implementation uses the “Empty Base Optimization” trick, which is usually the case for current STL implementations. - If using a zero-sized class is fine, the drawback is to write such a class for every custom deleter, which can be a nightmare to maintain.
The GenericDeleter<> shows how to capture the address of a function at compile time saving the payload overhead at runtime. - The resulting “template typedef” UniquePtr allows a concise automated writing of unique_ptr taking the address of a function deleter at compile-time.
- Another way to resolve the drawback of writing a class for every custom deleter is by using the function overload.
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 |
#include <iostream> #include <memory> #include <iostream> #include <functional> #define PRINT(x) std::cout << #x << ": " << x << std::endl void erase(double *pointer) { std::cout << "erase(double*" << pointer << ")" << std::endl; delete pointer; } struct DoubleEmptyDeleter { void operator()(double *p) { erase(p); } }; template <typename T, void (*deleter)(T*)> struct GenericDeleter { void operator()(T* pointer) const { deleter(pointer); } }; template <typename T, void (*deleter)(T*)> using UniquePtr = std::unique_ptr<T, GenericDeleter<T, deleter>>; struct SimpleGenericDeleter { template <typename T> void operator()(T* pointer) const { erase(pointer); } }; int main(int argc, char *argv[]) { PRINT(sizeof(std::unique_ptr<double>)); PRINT(sizeof(std::unique_ptr<double, void(*)(double*)>)); PRINT(sizeof(std::unique_ptr<double, std::function<void (double*)>>)); PRINT(sizeof(std::unique_ptr<double, DoubleEmptyDeleter>)); PRINT(sizeof(std::unique_ptr<double, GenericDeleter<double, &erase>>)); PRINT(sizeof(UniquePtr<double, &erase>)); PRINT(sizeof(std::unique_ptr<double, SimpleGenericDeleter>)); return 0; } |