본문 바로가기
C,C++/C++

[C++] placement new - 내가 원하는 메모리에 객체를 할당하고 싶다면

by woohyeon 2020. 12. 25.
반응형

Placement new


내가 지정한 메모리에 객체를 초기화할 수 있을까?

메모리 풀을 만들다가 난항을 겪어 c++ 포럼에 질문을 남겼다가 새로운 기능을 알게 되었다. 그때 그때 생성할 객체의 타입이 달라서 오브젝트 풀보단, 메모리 풀을 만들고 그 메모리 풀에 객체를 할당하고 싶었다.

즉 다음과 같이 char 타입의 메모리를 생성해 놓고, 앞으로 생성할 객체들이 해당 메모리를 사용하면 좋겠다고 생각했다. 그리고 그런 것은 불가능할 줄 알았는데 가능했다.

char* pMemoryPool = new char[1024];
size_t head = 0;

A* pA = (A*)(pMemoryPool + head);
head += sizeof(A);

B* pB = (B*)(pMemoryPool + head);
head += sizeof(B);

 

new 연산자는 기본적으로 C의 malloc과 달리 메모리 할당 + 초기화를 수행한다. 메모리 할당은 말 그대로 해당 클래스 타입의 인스턴스를 저장할 공간을 할당받는 것이고 초기화는 생성자를 호출함으로써 할당받은 메모리 공간에 인스턴스를 생성 및 초기화한다. C++에선 new의 이 두 과정을 분리하여 다룰 수 있다.

바로 placement new 라는 기능인데, 이 기능은 새롭게 힙 영역의 메모리를 가져다 쓰는 것이 아니라, 내가 사용하길 원하는 메모리 공간을 지정할 수 있다. 즉 기존에 존재하던 메모리 공간을 활용할 수 있다. 문법은 다음과 같이 간단하다.

A* pMemory = (A*)(pMemoryPool + head);
A* pA = new (pMemory) A; // placement new

new와 타입 사이에 괄호와 함께 사용할 메모리 공간을 적어주면 된다. 그러면 A 타입의 인스턴스가 pMemory에 초기화된다. 과정을 좀 더 자세하게 나누어 보면 다음과 같다.

  1. 인스턴스를 생성 및 초기화할 공간으로 pMemory를 지정한다.
  2. pMemory가 A의 생성자를 호출한다.
  3. pMemory에 A의 인스턴스가 생성 및 초기화된다.
  4. pMemory를 pA에 저장한다.

아래 두 사진은 지정한 메모리 주소를 통해 생성자를 호출하는 것을 확인시켜준다.

사용할 메모리 공간의 주소

 

위의 메모리 주소를 통해 생성자를 호출

 

한 가지 알아두어야 할 것은 placement new를 통해 생성된 객체는 delete로 지워선 안된다. 해당 객체의 소멸자를 직접 호출해야 한다. 그리고 당연히 스택 메모리에 생성된 배열은 사용할 수 없다.

https://en.cppreference.com/w/cpp/language/new#Placement_new

 

std::align


그런데 단순히 위와 같이 객체를 생성하면 안된다. 그 이유는 각 클래스마다 메모리 정렬 기준이 다르기 때문이다. 우리가 가져온 메모리의 타입은 char 타입으로, 1바이트(char) 정렬 메모리이다. 우리가 사용하는 클래스들이 모두 1바이트 정렬일 것이란 보장이 없으므로 인스턴스를 생성하기 전에 메모리 정렬을 우리가 사용할 클래스에 맞춰주어야 한다.

<메모리 정렬에 대해 모르면 해당 포스팅 참고>

이를 위해 std::align 함수를 사용해야 한다. 해당 함수의 프로토타입은 다음과 같다. ( <memory> 헤더 필요 )

void* align( std::size_t alignment, std::size_t size, void*& ptr, std::size_t& space );

해당 함수는 ptr로부터 size 바이트 만큼의 영역에 대해 alignment 기준으로 메모리를 정렬하고 ptr에 저장한다. space는 ptr로부터 사용 가능한 사이즈 공간을 의미한다. 정렬하게 되면 ptr이 변경이 되기 때문에 사용 가능한 공간이 바뀌게 된다. 변경된 크기가 space에 업데이트된다. 만약 공간이 적어 정렬할 수 없다면 nullptr을 반환한다. 성공 시 반환 값은 ptr에 저장되는 값과 동일하다.

char 타입의 메모리에서 사용할 주소를 가져와 T 타입의 객체를 할당하는 순서를 정리해보면 다음과 같다.

  1. 메모리 시작 주소를 가져온다.
  2. 해당 메모리 주소와 T 타입의 정보 등을 align에 전달하여 정렬시킨다.
  3. 정렬된 메모리 주소를 placement new에 사용하여 객체를 생성한다.

 위 과정을 코드로 표현해 보면 다음과 같다. 다음 코드는 char 타입의 메모리 풀로부터 주소를 가져와 클래스 A에 맞게 정렬하고 placement new를 통해 객체를 생성하는 예이다.

#include <memory>
using namespace std;

char* pMemoryPool = nullptr;

// 4바이트 정렬
class A
{
   char c;
   int  n;
}

void main()
{
   pMemoryPool = new char[maxSizeInByte];
   
   size_t maxSizeInByte = 1024;
   size_t head = 0; // pMemoryPool로부터 사용 가능한 위치까지의 offset
   size_t availableSize = maxSizeInByte - head;
   
   // 메모리를 정렬합니다.
   // alignof는 해당 타입의 정렬 기준 바이트를 반환합니다.
   // i.e. alignof(A) => 4, alignof(B) => 2
   void* pMemory = pMemoryPool + head;
   A* pAlignedMemory = nullptr;
   if(align(alignof(A), sizeof(A), pMemory, availableSize))
   {
      // 정렬에 성공했다면
      pAlignedMemory = reinterpret_cast<A*>(pTargetMemory);
      head += sizeof(A);
   }
   
   // placement new
   A* pA = new (pAlignedMemory) A();
   
   // delete를 사용하면 안됨
   // pA 사용이 끝나면 소멸자를 호출
   pA->~A();
}

 

위 내용을 적절히 나누어서 템플릿 함수로 만들 수 있다. 가변인자 템플릿을 사용하면 new에 가변인자를 전달하여 T 타입 객체를 생성할 수도 있다.

좀 더 자세한 구현이 궁금하면 github.com/wooPedia/Task-MemoryPool 

 

참고자료

en.cppreference.com/w/cpp/memory/align

en.cppreference.com/w/cpp/language/new#Placement_new

3dmpengines.tistory.com/1399

www.kdata.or.kr/info/info_04_view.html?field=&keyword=&type=techreport&page=5&dbnum=188824&mode=detail&type=techreport




댓글