[C++ Memory Management] Allocator

1. allocator

The allocator is one of the cornerstones of the C++ STL library. It is a strategy pattern that allows users to decouple memory management from the container and perform more specific operations. By using allocator, we can customize how memory is allocated and released, giving us better control over memory usage.

1.1 Why use allocator

In C++, memory application and release is an expensive operation. Frequent application and release may cause system memory fragmentation and degrade program performance. By using allocator, we can customize the memory application and release methods, reduce system memory fragmentation, and improve program performance.

In addition, allocator also plays an important role, which is to separate the construction of objects and the application of memory. In the traditional memory application method, we will call the constructor of the object when applying for memory. But sometimes, we may just want to apply for memory and do not want to construct the object immediately. In this case, we can use allocator.

1.2 Basic use of allocator

In C++ STL, allocator is a template class, and we can create an allocator of a specific type by providing it with a type parameter. Here's a basic example:

#include <memory>

int main() {
    
    
    std::allocator<int> alloc; // 创建一个分配int的allocator
    int* p = alloc.allocate(10); // 分配10个int的空间

    // 使用未构造的内存
    for (int i = 0; i < 10; ++i) {
    
    
        alloc.construct(p + i, i); // 在分配的内存上构造对象
    }

    // 销毁对象并释放内存
    for (int i = 0; i < 10; ++i) {
    
    
        alloc.destroy(p + i); // 销毁对象
    }
    alloc.deallocate(p, 10); // 释放内存
    return 0;
}

1.3 Custom allocator

By customizing the allocator, we can more flexibly control the application and release of memory. For example, we can store vector data directly into a database, shared memory, or file to achieve data persistence and sharing.

A custom allocator needs to provide the following interfaces:

  • typedefs: Define aliases for the types used
  • allocate(n): Allocate memory that can accommodate n objects
  • deallocate(p, n): Release previously allocated memory
  • construct(p, val): Construct an object on the memory pointed to by pointer p, whose value is val
  • destroy(p): Destroy the object pointed to by pointer p

Here is an example of a simple custom allocator:

template <class T>
class MyAllocator {
    
    
public:
    typedef T value_type;

    MyAllocator() = default;
    template <class U> constexpr MyAllocator(const MyAllocator<U>&) noexcept {
    
    }

    T* allocate(std::size_t n) {
    
    
        return static_cast<T*>(::operator new(n*sizeof(T)));
    }

    void deallocate(T* p, std::size_t) noexcept {
    
    
        ::operator delete(p);
    }
};

template <class T, class U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) {
    
     return true; }

template <class T, class U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) {
    
     return false; }

In this example, we create a custom allocator MyAllocatorthat uses the global newAND deleteoperator to allocate and free memory.

2. Distributor overview

2.1 The role and importance of the allocator

Allocators play a crucial role in C++. When they are used to implement container algorithms, they can be isolated and decoupled from storage details. The advantage of using an allocator is that developers can focus on the implementation of the algorithm without having to worry about memory management. Not only that, the allocator also provides us with standard methods for storage allocation and release, as well as some functions for object construction and destruction.

2.2 Standard allocator in STL

The C++ STL library provides a standard allocator: std::allocator, which implements the most basic memory allocation and release strategy. In most cases, its performance is high enough, but in some special cases (such as the allocation and destruction of a large number of small objects), you may get better performance using a custom allocator.

2.3 Use of allocators

The code below shows how to use it std::allocator. Among them, allocateit is used to allocate memory, constructused to construct objects on allocated memory, destroyused to destroy objects, and deallocateused to release memory. It should be noted that starting from C++17, the constructand destroyfunctions have been deprecated and we need to use them std::allocator_traitsto call construction and destruction.

#include <memory>

int main() {
    
    
    std::allocator<int> alloc; // 创建一个分配int的allocator
    int* p = alloc.allocate(10); // 分配10个int的空间

    // 使用未构造的内存
    for (int i = 0; i < 10; ++i) {
    
    
        std::allocator_traits<std::allocator<int>>::construct(alloc, p+i, i);
    }

    // 销毁对象并释放内存
    for (int i = 0; i < 10; ++i) {
    
    
        std::allocator_traits<std::allocator<int>>::destroy(alloc, p+i);
    }
    alloc.deallocate(p, 10); // 释放内存

    return 0;
}

In the above code, first we create an allocator that allocates ints and allocates space for 10 ints. We then use std::allocator_traitsthe constructmethod to construct the object on the allocated memory. Finally, we destroy the object using std::allocator_traitsthe destroymethod and deallocatefree the memory using the method.

3. Custom allocator

The flexibility of the C++ STL library mainly comes from its strategy pattern design, and the allocator is an important application of this design. By customizing the allocator, we can implement some special memory management strategies, such as memory sharing, memory leak detection, pre-allocated object storage, memory pools, etc.

3.1 Application scenarios of custom allocators

Listed below are some application scenarios for custom allocators:

  1. Memory sharing : For multi-process or multi-threaded applications, we may need to share memory space. A custom allocator allows us to store objects in shared memory.

  2. Memory leak detection : In complex applications, memory leaks can be a difficult problem to locate. Custom allocators can help us track memory allocation and deallocation to detect memory leaks.

  3. Pre-allocated object storage : For some applications that know their memory requirements, pre-allocating memory can avoid frequent memory allocation and release and improve performance.

  4. Memory pool : For applications that frequently allocate and release small blocks of memory, using a memory pool can reduce memory fragmentation and improve performance.

3.2 Implementation of custom allocator

A custom allocator needs to implement the following interfaces:

  • typedefs: Define aliases for the types used
  • allocate(n): Allocate memory that can accommodate n objects
  • deallocate(p, n): Release previously allocated memory
  • construct(p, val): Construct an object on the memory pointed to by pointer p, whose value is val
  • destroy(p): Destroy the object pointed to by pointer p

The following code demonstrates how to implement a custom allocator:

template <class T>
class MyAllocator {
    
    
public:
    typedef T value_type;

    MyAllocator() = default;
    template <class U> constexpr MyAllocator(const MyAllocator<U>&) noexcept {
    
    }

    T* allocate(std::size_t n) {
    
    
        // 你的内存分配策略
    }

    void deallocate(T* p, std::size_t) noexcept {
    
    
        // 你的内存释放策略
    }

    template<typename... Args>
    void construct(T* p, Args&&... args) {
    
    
        // 你的对象构造策略
    }

    void destroy(T* p) {
    
    
        // 你的对象销毁策略
    }
};

template <class T, class U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) {
    
     return true; }

template <class T, class U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) {
    
     return false; }

3.3 Use of custom allocators

Custom allocators can be used for any container in STL, including vector, list, etc. Here is an example of a vector using a custom allocator:

#include <vector>
#include "MyAllocator.h" // 包含你的自定义分配器的头文件

int main() {
    
    
    std::vector<int, MyAllocator<int>> vec; // 使用自定义分配器的vector
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
    return 0;
}

In this example, we create one MyAllocatorusing std::vector. Therefore, vectorthe memory management strategy for this will be determined by ours MyAllocator. The same approach can be applied to std::listother STL containers.

4. Uninitialized memory algorithm

In C++ STL, there are a series of uninitialized memory algorithms, which are used to directly construct objects on uninitialized memory, which can improve the efficiency of the program. The names of these algorithms usually uninitialized_start with , with uninitialized_copybeing the most commonly used one.

4.1 uninitialized_copy algorithm

uninitialized_copyis an algorithm for copying sequences on uninitialized memory. It accepts two input iterators (defining the sequence to be copied) and an output iterator (defining the starting position of uninitialized memory), and attempts to construct the same elements in the output range as the input sequence.

Here are uninitialized_copythe basic uses of :

#include <memory>
#include <vector>

int main() {
    
    
    std::vector<int> vec {
    
    1, 2, 3, 4, 5};
    std::allocator<int> alloc;

    // 使用 allocator 分配未初始化内存
    int* p = alloc.allocate(vec.size());

    // 使用 uninitialized_copy 将 vec 中的元素复制到未初始化的内存中
    std::uninitialized_copy(vec.begin(), vec.end(), p);

    // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源
    for (std::size_t i = 0; i < vec.size(); ++i) {
    
    
        alloc.destroy(p + i);
    }
    alloc.deallocate(p, vec.size());

    return 0;
}

In the above code, we first create a containing five integers vector. We then use to allocatorallocate a block vectorof uninitialized memory large enough to store all the elements in . Next, we use to uninitialized_copycopy vectorthe elements in to this uninitialized memory. Finally, we iterate over the memory, calling each element destroy, and then call to deallocatefree the entire memory.

It should be noted that since uninitialized_copydestructor and deallocate are not automatically called, we need to call them manually to prevent memory leaks.

4.2 uninitialized_copy_n algorithm

uninitialized_copy_nis uninitialized_copya variant that accepts an input iterator (defining the starting position of the sequence to be copied), a size value n (defining the number of elements to be copied) and an output iterator (defining the uninitialized memory starting position) and attempts to construct the same elements in the output range as the first n elements of the input sequence.

Here are uninitialized_copy_nthe basic uses of :

#include <memory>
#include <vector>

int main() {
    
    
    std::vector<int> vec {
    
    1, 2, 3, 4, 5};
    std::allocator<int> alloc;

    // 使用 allocator 分配未初始化内存
    int* p = alloc.allocate(vec.size());

    // 使用 uninitialized_copy_n 将 vec 中的前3个元素复制到未初始化的内存中
    std::uninitialized_copy_n(vec.begin(), 3, p);

    // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源
    for (std::size_t i = 0; i < 3; ++i) {
    
    
        alloc.destroy(p + i);
    }
    alloc.deallocate(p, vec.size());

    return 0;
}

In the above code, we first create a containing five integers vector. We then use to allocatorallocate a block vectorof uninitialized memory large enough to store all the elements in . Next, we use to copy the uninitialized_copy_nfirst vector3 elements in to this uninitialized memory. Finally, we iterate over the memory, calling for each element copied destroy, and then call to deallocatefree the entire memory.

It should be noted that since uninitialized_copy_ndestructor and deallocate are not automatically called, we need to call them manually to prevent memory leaks. This is uninitialized_copythe same as .

4.3 uninitialized_fill algorithm

uninitialized_fillis an algorithm for filling uninitialized memory with values. It takes two iterators (defining a range of uninitialized memory) and a value, and then attempts to construct this value within the specified range.

Here are uninitialized_fillthe basic uses of :

#include <memory>

int main() {
    
    
    std::allocator<int> alloc;

    // 使用 allocator 分配未初始化内存
    int* p = alloc.allocate(5);

    // 使用 uninitialized_fill 将值42填充到未初始化的内存中
    std::uninitialized_fill(p, p + 5, 42);

    // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源
    for (std::size_t i = 0; i < 5; ++i) {
    
    
        alloc.destroy(p + i);
    }
    alloc.deallocate(p, 5);

    return 0;
}

In the above code, we use to allocatorallocate a piece of uninitialized memory that can store 5 integers. Then, we uninitialized_fillfill the uninitialized memory with the value 42. Finally, we iterate over the memory, calling each element destroy, and then call to deallocatefree the entire memory.

It should be noted that since uninitialized_filldestructor and deallocate are not automatically called, we need to call them manually to prevent memory leaks.

The usage of other uninitialized memory algorithms ( uninitialized_fill_n, uninitialized_default_construct, uninitialized_value_construct) is uninitialized_copysimilar to . You also need to pay attention to manually calling destructor and deallocate to prevent memory leaks.

5. construct_at, destroy_at object construction and destruction

In C++17 and C++20, there are two very important functions: std::construct_atand std::destroy_at. These two functions can construct and destroy objects at a given memory location respectively.

5.1 std::construct_at

std::construct_atIs a method of constructing an object at a specified memory location. It accepts a pointer and a series of constructor parameters, and then constructs an object at the memory location pointed to by the pointer.

Here are std::construct_atthe basic uses of :

#include <memory>

struct MyStruct {
    
    
    int x;
    float y;
    MyStruct(int x, float y) : x(x), y(y) {
    
    }
};

int main() {
    
    
    std::allocator<MyStruct> alloc;

    // 使用 allocator 分配未初始化内存
    MyStruct* p = alloc.allocate(1);

    // 使用 construct_at 在未初始化的内存中构造一个 MyStruct 对象
    std::construct_at(p, 42, 3.14f);

    // 使用完成后,需要手动调用 destroy_at 和 deallocate 释放资源
    std::destroy_at(p);
    alloc.deallocate(p, 1);

    return 0;
}

In the above code, we first define a MyStructstructure called . We then use to allocatorallocate a block MyStructof uninitialized memory large enough to store an object. Next, we use to construct an object construct_aton this uninitialized memory . MyStructFinally, we call to destroy_atdestroy the object, and then call to deallocaterelease the entire memory.

5.2 std::destroy_at

std::destroy_atIs a method of destroying an object at a specified memory location. It takes a pointer and then calls the destructor of the object pointed to by the pointer.

In the above std::construct_atpart of the code, we have shown std::destroy_atthe basic usage of . Here is another independent example:

#include <memory>

struct MyStruct {
    
    
    int x;
    float y;
    MyStruct(int x, float y) : x(x), y(y) {
    
    }
    ~MyStruct() {
    
    
        // 自定义析构函数
        std::cout << "MyStruct object is being destroyed.\n";
    }
};

int main() {
    
    
    std::allocator<MyStruct> alloc;

    // 使用 allocator 分配未初始化内存
    MyStruct* p = alloc.allocate(1);

    // 使用 construct_at 在未初始化的内存中构造一个 MyStruct 对象
    std::construct_at(p, 42, 3.14f);

    // 使用 destroy_at 销毁这个对象
    std::destroy_at(p);

    // 使用完成后,需要手动调用 deallocate 释放资源
    alloc.deallocate(p, 1);

    return 0;
}

In the above code, we first define a MyStructstructure called which has a custom destructor. We then use to allocatorallocate a block MyStructof uninitialized memory large enough to store an object. Next, we use to construct an object construct_aton this uninitialized memory . MyStructNext, we call to destroy_atdestroy this object, and you can see the output information of the custom destructor. Finally, we call to deallocatefree the entire memory.

Overall, std::construct_atand std::destroy_atprovide a convenient, safe way to construct and destroy objects at specified memory locations, and they provide better control than using directly newand delete, especially when dealing with uninitialized memory. .

Guess you like

Origin blog.csdn.net/weixin_52665939/article/details/131620359