In the last article, we introduced how containers use std::allocator. But what if we want to use std::allocator directly?Typically, we do not use std::allocator methods directly, but rather through a utility class (pseudo code below, Code Example 1):
// Code Example 1
namespace std
{
template<class Alloc>
struct allocator_traits {
// Alloc is usually template<class T> class allocator
// or its subclasses
// Thus, type T is usually referred to as std::allocator::value_type
// These two are wrappers for the Alloc::allocate method
static Alloc::value_type* allocate(Alloc& a, std::size_t n);
static Alloc::value_type* allocate(Alloc& a, size_type n, const void* hint);
// This is a wrapper for the Alloc::deallocate method
static void deallocate(Alloc& a, Alloc::value_type* p, std::size_t n);
// This method is a wrapper for initializing p -- calling the constructor
template<class... Args>
static void construct(Alloc& a, Alloc::value_type* p, Args&&... args);
// This method is a wrapper for calling the destructor of p
static void destroy(Alloc& a, Alloc::value_type* p);
};
}
Now let’s look at how to use it (Code Example 2):
// Code Example 2
#include <cstdlib>
#include <iostream>
#include <memory>
class Data {
public:
Data(int v): v_(v) {
std::cout << "Data constructor: " << reinterpret_cast<void*>(this) << std::endl;
}
~Data() {
std::cout << "Data destructor: " << reinterpret_cast<void*>(this) << std::endl;
}
private:
int v_;
};
int main()
{
std::allocator<Data> alloc;
// Suppose there is an instance of std::allocator
// For example
// std::allocator<int> other_alloc;
// using AllocType = std::allocator_traits<decltype(other_alloc)>::rebind_alloc<Data>;
// AllocType alloc;
// Allocate memory
Data* d = std::allocator_traits<decltype(alloc)>::allocate(alloc, 1);
std::cout << "Allocated memory address: " << reinterpret_cast<void*>(d) << std::endl;
// Call constructor
// Here std::allocator_traits<decltype(alloc)>::construct(alloc, d); will fail to compile
// This is because the Data class does not have a default constructor
std::allocator_traits<decltype(alloc)>::construct(alloc, d, 10);
std::cout << "==================" << std::endl;
// Call destructor
std::allocator_traits<decltype(alloc)>::destroy(alloc, d);
std::cout << "Freed memory address: " << reinterpret_cast<void*>(d) << std::endl;
// Free memory
std::allocator_traits<decltype(alloc)>::deallocate(alloc, d, 1);
}
The output is as follows:
Allocated memory address: 0x7f9c8c4059b0
Data constructor: 0x7f9c8c4059b0
==================
Data destructor: 0x7f9c8c4059b0
Freed memory address: 0x7f9c8c4059b0
In fact, the container classes of the C++ standard library also call instances of std::allocator (or its subclasses) through std::allocator_traits.std::allocator_traits can only initialize one at a time, but there is a method for batch initialization, as shown below (Code Example 3):
// Code Example 3
#include <iostream>
#include <list>
#include <memory>
#include <vector>
struct Data {
Data() : v_(-1) {}
Data(int v): v_(v) {}
int v_;
};
int main()
{
const std::size_t data_count = 4;
std::allocator<Data> alloc;
// In fact, most of the time we use the following
// Data* p = alloc.allocate(data_count);
// Simple and direct
Data* p = std::allocator_traits<decltype(alloc)>::allocate(alloc, data_count);
// Print data
auto print_p = [&p] {
for(std::size_t index = 0; index < data_count; ++index) {
std::cout << (p + index)->v_;
if (index + 1 == data_count) std::cout << std::endl;
else std::cout << ", ";
}
};
// Print uninitialized data -- just to prove it is uninitialized
std::cout << "Uninitialized: ";
print_p();
// We can consider p's type as Data[4]
// Here we initialize p[1] -- using the initialization function Data()
std::allocator_traits<decltype(alloc)>::construct(alloc, p + 1);
// Here we initialize p[2] -- using the initialization function Data(16)
std::allocator_traits<decltype(alloc)>::construct(alloc, p + 2, 16);
std::cout << "Partially initialized: ";
print_p();
// Batch initialization
std::uninitialized_fill_n(p, data_count, Data(10));
// This is equivalent to the following line
// std::uninitialized_fill(p, p + data_count, Data(10));
// This batch initialization uses Data's copy constructor
// Additionally, calling this batch initialization will cause p[1] and p[2] to be initialized twice
// Will initializing twice (i.e., calling the constructor twice) cause problems?
// In fact, constructors and destructors are essentially no different from other functions
// Whether multiple calls are problematic depends entirely on the implementation of the function -- that is, what it actually does
// Here, this is certainly not a problem
// By the way, Data does not explicitly define a copy constructor, it is generated by the compiler by default
std::cout << "Batch initialization: ";
print_p();
// Free memory
// Note: Here, the destructor is not called before freeing either
// Just like construction, not calling the destructor here will not be a problem
std::allocator_traits<decltype(alloc)>::deallocate(alloc, p, data_count);
}
The output is as follows (uninitialized values may differ under different compilation conditions):
Uninitialized: 0, 0, 0, 0
Partially initialized: 0, -1, 16, 0
Batch initialization: 10, 10, 10, 10