dh_0e

[C++/Game Server] Allocator (New, Delete Operator Overloading) + std::forward, Placement New 본문

C++/Game Server

[C++/Game Server] Allocator (New, Delete Operator Overloading) + std::forward, Placement New

dh_0e 2026. 3. 10. 12:26

New, Delete Operator

  • 다음과 같이 new와 delete도 operator로 재정의 가능
    • operator로 흐름을 가로채서 로그를 찍거나 무언가를 할 수 있음
class Knight {
public:
	Knight() { cout << "Knight()" << endl; }
	~Knight() { cout << "~Knight()" << endl; }

	int32 _hp = 100;
	int32 _mp = 100;
};

// new operator overloading (Global)
void* operator new(size_t size) {
	cout << "operator new: " << size << endl;
	void* ptr = malloc(size);
	return ptr;
}

void operator delete(void* ptr) {
	cout << "operator delete: " << ptr << endl;
	::free(ptr);
}

void* operator new[](size_t size) {
	cout << "operator new: " << size << endl;
	void* ptr = malloc(size);
	return ptr;
}

void operator delete[](void* ptr) {
	cout << "operator delete: " << ptr << endl;
	::free(ptr);
}

int main() {
	Knight* knight = new Knight();
	delete knight;
	return 0;
}

실행 결과

  • 다음과 같이 전역으로 선언하면 Global 하게(모든 new, delete에) 적용돼서 위험할 수 있음
  • 아래와 같이 클래스에 넣어서 해당 클래스에만 적용되게끔 사용이 가능함
class Knight {
public:
	Knight() { cout << "Knight()" << endl; }
	~Knight() { cout << "~Knight()" << endl; }

	void* operator new(size_t size) {
    	// == static void* operator new() {}
    	// 클래스 멤버로 선언 시 자동으로 static 함수가 됨
		cout << "operator new: " << size << endl;
		void* ptr = malloc(size);
		return ptr;
	}

	void operator delete(void* ptr) {
		cout << "operator delete: " << ptr << endl;
		::free(ptr);
	}

	int32 _hp = 100;
	int32 _mp = 100;
};

 

Allocator.h / Allocator.cpp

  • Alloc(): 메모리 할당만 하고 생성자는 직접 호출해주지 않음
  • Release(): 메모리 해제만 담당하고 소멸자는 직접 호출해주지 않음

 

Memory.h

#pragma once
#include "Allocator.h"

template <typename Type, typename... Args>
Type* xnew(Args&&... args) {  // 새로운 new 연산자 오버로딩
	Type* memory = static_cast<Type*>(xxalloc(sizeof(Type)));

	// placement new: 어떤 객체의 생성자를 호출하는 또다른 문법
	// new (memory) Type();
	new (memory) Type(std::forward<Args>(args)...);
	// forward: 가변인자를 통해 생성자를 호출
	return memory;
}

template <typename Type>
void xdelete(Type* obj) {
	obj->~Type();  // 소멸자 직접 호출
	xxrelease(obj);
}

 

Memory.cpp

#pragma once
#include "Allocator.h"

template <typename Type, typename... Args>
Type* xnew(Args&&... args) {  // 새로운 new 연산자 오버로딩
	Type* memory = static_cast<Type*>(xxalloc(sizeof(Type))); // xxalloc == BaseAllocator::Alloc

	// placement new: 어떤 객체의 생성자를 호출하는 또다른 문법
	// new (memory) Type();
	new (memory) Type(std::forward<Args>(args)...);
	// forward: 가변인자를 통해 생성자를 호출
	return memory;
}

template <typename Type>
void xdelete(Type* obj) {
	obj->~Type();  // 소멸자 직접 호출
	xxrelease(obj); // xxrelease == BaseAllocator::Release
}
  • Allocator.h/.cpp로 다음과 같이 메모리 할당, 해제 로직을 만들고 Memory.h/.cpp새로운 xnew, xdelete를 제작
  • xnew()
    • xnew(Args&&... args)에서 &&는 전달 참조(Forwarding Reference)로 Rvalue(임시 객체)를 넣든, Lvalue(변수)를 넣든 다 받아낼 수 있음
      • 하지만 함수 내부로 들어오는 순간, 이름(args)이 생기기 때문에 이 변수는 무조건 Lvalue가 됨
    • 가변 인자를 받아 다양한 생성자 종류에 대응할 수 있도록 std::forward 사용
      • std::forward: 전달받은 인자의 원래 성질(Lvalue인지 Rvalue인지)을 그대로 유지해서 다음 함수로 넘겨주는 도구
        • Lvalue가 들어올 때: Args가 int&가 되고, 참조 축약 규칙에 의해 int&를 반환(복사 생성자 호출)
        • Rvalue가 들어올 때: Args가 int가 되고, std::forward는 내부적으로 static_cast<int&&>(args)를 수행 (이동 생성자 호출)
      • 생성자에 필요한 인자가 몇 개든 상관없이 원래 형태 그대로 생성자에게 배달해 주는 역할
      • 이게 없으면 모든 인자가 복사(Copy)되거나 Lvalue로 전달되어, 성능 최적화(Move)가 불가능해짐
        • std::move: 무조건 Rvalue로 바꿈 (강제 약탈)
        • std::forward: 조건부로 Rvalue로 바꿈 (원래 성격 유지)
    • Allocator(Alloc)에서 생성자를 호출하지는 않기 때문에 placement new를 사용하여 생성자 호출 (with 가변 인자)
      • Placement New: 이미 확보된 메모리 주소에 객체의 생성자만 호출해서 앉히는(placement) 기법
        • 일반 new: [메모리 할당 + 생성자 호출]
        • Plcement new: [생성자 호출]만 담당
          • 해제할 때 소멸자 직접 호출해줘야 함
          • !Placement new로 생성한 객체는 절대 delete로 지우면 안 됨!

Placement New 문법

#include <new> // 반드시 이 헤더를 포함해야 함

// 1. 메모리 준비 (보통 할당이나 배열로 준비)
void* ptr = malloc(sizeof(Knight)); 

// 2. Placement New 호출
// new (주소) 타입(생성자 인자);
Knight* knight = new (ptr) Knight();
  • xdelete()
    • Allocator(Release)에서 소멸자를 호출하지 않기 때문에 메모리 할당을 해제하기 전에 소멸자 호출
  • xxalloc, xxreleasecoreMacro에 정의

 

coreMacro.h

/*----------------
		Memory
----------------*/

#ifdef _DEBUG
#define xxalloc(size) BaseAllocator::Alloc(size)
#define xxrelease(ptr) BaseAllocator::Release(ptr)
#else
#define xxalloc(size) BaseAllocator::Alloc(size)
#define xxrelease(ptr) BaseAllocator::Release(ptr)
#endif
  • Debug 상황, Release 상황에 맞게 다양한 alloc, release 버전을 만들어 사용 가능하게끔 매크로 생성

 

GameServer.cpp

class Knight {
public:
	Knight() { cout << "Knight()" << endl; }
	Knight(int32 hp) : _hp(hp) { cout << "Knight(int32 hp): " << hp << endl; }
	~Knight() { cout << "~Knight()" << endl; }
    
	int32 _hp = 100;
	int32 _mp = 100;
};


int main() {
	Knight* knight = xnew<Knight>(100);
    
	xdelete(knight);
	return 0;
}

실행 결과

  • 다음과 같이 인수가 잘 전달되어 출력되는 것을 확인할 수 있음