본문으로 바로가기

Operator new와 Allocator에 대한 이해

category Project/STL 구현 2022. 6. 19. 19:20

메모리 할당과 생성자 호출의 분리

C++에서 객체를 생성할 때 new를 쓸 수 있다. 그러나 new를 사용할 경우 os로부터 메모리를 얻어옴과 동시에 반드시 해당 메모리에 객체를 생성하고 초기화하게 된다. 하지만 컨테이너를 구현하기 위해서는 메모리를 얻어오는 기능과 객체를 생성하는 기능을 분리할 필요가 있다.

 

예를 들어 vector를 살펴보자. vector의 size는 실제 생성된 객체의 수를 나타내고, capacity는 os로부터 얻어온 메모리 공간의 수를 나타낸다. 그런데, new를 사용할 경우 os로부터 객체를 생성하지 않고 메모리만 얻어올 수 있는 방법이 없다. 그래서 capacity라는 개념도 존재할 수 없게 된다.

 

operator new와 placement new

 

메모리 할당과, 객체의 생성을 분리하기 위해서 new의 기능을 operator new()와 placement new를 분리해서 사용할 수 있다.

  • operator new(): 원하는 크기만큼 메모리를 할당받을 수 있다.
  • placement new: 할당한 메모리 주소에 생성자를 호출한다.

반대로 객체의 소멸자를 호출하는 기능과, 메모리 해제하는 기능은 다음의 방식을 활용할 수 있다.

  • obj.~T()
  • operator delete()
// operator new: data에 객체에 메모리만 할당
Data* data = static_cast<Data*>( operator new(sizeof(Data))); 

// placement new: 할당된 메모리(data)에 Data생성자 호출
new(data) Data;

//  data 의 소멸자만 호출
data.~Data();

// data의 메모리 해제
operator delete(data);

Allocator에 대한 이해

Allocator란 노드 기반 컨테이너에 대해 메모리 블록을 할당 및 해제하는 데 도움이 되는 템플릿들을 말한다. STL 컨테이너들 내부에서는 직접 operator new나 placement new를 호출하는 것이 아니라 이들을 랩핑하고 있는 Allocator를 통해서 메모리 할당과 생성자 호출을 진행한다. 다시 말해 Allocator는 메모리 할당과 해제를 추상화 한 도구이다. 덕분에 컨테이너 사용자가 컨테이너의 메모리 할당과 해제 방식을 변경하고자 한다면 컨테이너를 생성할 때 템플릿 인자로 원하는 Allocator를 전달하면 된다.

 

Allocator 또한 메모리 할당과 생성자 호출의 기능을 분리하여 사용하고 있기 때문에 사용법은 operator new - placement new와 사용법이 비슷하다. 

  • allocate(): 메모리 할당
  • construct(): 생성자 호출(C++ 20 이전까지만 가능)
  • destroy(): 소멸자 호출(C++ 20 이전 까지만 가능)
  • deallocate(): 메모리 반환

Allocator 사용 예시

#include <iostream>

struct Data {
    Data(int num) : index(num)
    {
        std::cout << "Data(): " << index << std::endl;
    }
    ~Data()
    {
        std::cout << "~Data(): " << index << std::endl;
    }
    int index;
};

int main()
{
    std::cout << "std::allocator<Data>'s size: "<<sizeof(std::allocator<Data>) << std::endl;

    std::allocator<Data> alc;

    constexpr size_t size{ 4 };

    Data* pData = alc.allocate(size);    // pData에 Data 10개 크기의 메모리 할당
    std::cout << "after allocate" << std::endl;

    for (int i = 0; i < size; i++)
    {
        alc.construct(pData + i, i);    // pData에 Data 10개 생성(생성자 호출) C++20 이전까지만
    }
    std::cout << "after construct" << std::endl;


    for (int i = 0; i < size; i++)
    {
        std::cout << pData[i].index <<": " << &pData[i] << std::endl;
    }

    for (int i = 0; i < size; i++)
    {
        alc.destroy(pData + i);    // pData에 Data 10개 소멸(소멸자 호출) C++20 이전 까지만
    }
    std::cout << "after destroy" << std::endl;
    alc.deallocate(pData, size);    // pData에 Data 10개 크기의 메모리 반환
    std::cout << "after deallocate" << std::endl;

    return 0;
}

출력 결과

std::allocator<Data>'s size: 1
after allocate
Data(): 0
Data(): 1
Data(): 2
Data(): 3
after construct
0: 01185798
1: 0118579C
2: 011857A0
3: 011857A4
~Data(): 0
~Data(): 1
~Data(): 2
~Data(): 3
after destroy
after deallocate

 

STL의 컨테이너들을 구현하기 위해서는 Allocator에 대한 이해와 사용법에 대한 숙지가 필요하다. 아래의 자료들을 참고하자.

https://wikidocs.net/29955

https://en.cppreference.com/w/cpp/memory/allocator

https://docs.microsoft.com/ko-kr/cpp/standard-library/allocators-header?view=msvc-170