template<class T,                               // 컨테이너 원소의 타입
        class Allocator = std::allocator<T>>    // ????? 어떤 기본 값을 갖는다.
class vector;

   위와 코드는 vector컨테이너 클래스의 원형이다. 해석하면 T 는 컨테이너에 저장될 원소의 타입을 말한다. 그런데, Allocator라는 알 수 없는 녀석이 있다. 이런식으로 어떤 맥락에서 알 수 없는 요소는 개념을 이해하기 불편하고, 글을 읽기 어렵게 만든다고 생각한다. 그래서, 먼저 Allocator에 대해 간략하게 설명하고자 한다.


 

1. Allocator란?

   array를 제외한 모든 컨테이너는 동적으로 할당/해제되는 구조를 가졌다. 동적할당을 위해서는 할당과 해제에 관련된 연산이 필요한데, Allocator는 이 할당/해제 연산을 어떻게 할 것인가를 정의한 것이다. 컨테이너는 Allocator에 따라 자신에게 필요한 메모리를 할당하고, 해제한다.

   우리는 정말 특수한 경우가 아니라면, Allocator를 직접 만들거나 신경 쓸 일은 없다. 왜냐하면, std에서는 기본적으로 사용할 수 있는 Allocator를 제공해주기 떄문이다. 위 Allocator의 기본값인 std::allocator<T>가 바로 std에서 기본적으로 제공해주는 할당 구조체이다. std::allocator<T>는 내부적으로 new와 delete 연산을 이용해서 할당/해제 연산을 수행한다. 따라서, 아래 내용은 생략하고 넘어가도 좋다.


2. 별도의 Allocator 필요한 경우?

   그러나, Allocator가 밖으로 드러나 있다는 것은 직접 Allocator를 만들어야할 경우가 있다는 것이다. 아마 이런 경우를 생각해볼 수 있을 것이다. (상황이 딱히 생각이 안나서 chatGPT의 도움을 일부 빌렸다.)

  1. 어떤 레거시 코드에 이미 C-Style로 할당을 처리하는 경우
  2. 런타임에 돌아가는 가비지 콜렉터나, 직접 힙 메모리를 관리하는 경우
    • 넉넉한 메모리를 우선 할당한 다음, 할당과 해제의 오버헤드를 줄이면서 직접 메모리를 관리해서 최적화 하거나, 별도의 GC를 직접 구현해 별도의 메모리 관리 정책이 있는 경우 이를 위한 별도의 Allocator를 작성해야할 수 있다.
  3. 기타 제약으로 인해서 allocator를 사용할 수 없을 때.
    • 하드웨어 제약이나, 별도의 외부 메모리를 사용하고 이에 대한 로직이 따로 있는 경우 기본 allocator를 사용할 수 없기 때문에 어쩔 수 없이 Allocator를 만들어야 하는 경우가 있을 수 있다.

3. Allocator 정의

   Allocator는 다음 4가지 연산 및 메서드를 반드시 구현해야 한다.

  1. T를 내부에서 value_type로 다시 타입 정의 할 것
  2. operator==, operator!=
    • 필수 연산이라고 되어있었지만, 단순한 vector 삽입 삭제에는 구현하지 않아도 문제 없었음.
  3. allocator()
  4. deallocator()
  5. 복사 생성자 (변환을 위한)
    • MS 문서에는 필수라고 되어있지 않지만, 없는 경우 컴파일 에러 발생

MS 공식 문서에서 제공하는 C스타일 형식 Allocator 예제를 약간만 수정했다.

  • Mallocator.h
// C-style Allocator
// https://learn.microsoft.com/en-us/cpp/standard-library/allocators?view=msvc-170#writing-your-own-allocator-c11

#pragma once
#include <stdlib.h> //size_t, malloc, free
#include <new> // bad_alloc, bad_array_new_length
#include <memory>

template <class T>
struct Mallocator
{
    typedef T value_type;  // value_type 정의
    Mallocator() noexcept {}

    // 변환을 허용하는 복사생성자
    template<class U> Mallocator(const Mallocator<U>&) noexcept {}

    T* allocate(const size_t n) const;
    void deallocate(T* const p, size_t) const noexcept;
};

template <class T>
T* Mallocator<T>::allocate(const size_t n) const
{
    if (n == 0)
    {
        return nullptr;
    }
    if (n > static_cast<size_t>(-1) / sizeof(T))
    {
        throw std::bad_array_new_length();
    }
    void* const pv = malloc(n * sizeof(T));
    if (!pv) { throw std::bad_alloc(); }

    return static_cast<T*>(pv);
}

template<class T>
void Mallocator<T>::deallocate(T* const p, size_t) const noexcept
{
    free(p);
}
  • main.cpp
#include<iostream>
#include<vector>

#include"Mallocator.h"
using namespace std;

int main() {
    vector<int, Mallocator<int>> v1;
    // 삽입 중에 할당 및 재할당과 해제 과정이 존재
    v1.push_back(1);
    v1.push_back(2);
    v1.push_back(3);
    v1.push_back(4);
    v1.push_back(5);

    for (auto& v : v1) {
        cout << v << ' ';
    }
    v1.clear();
}