본문 바로가기
서적 정리/Effective C++

51.new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자

by 민돌이2 2022. 1. 2.

무언가를 커스터마이징을 할 때 그 대상의 기본적인 기능은 갖춘 상태에서 입맛에 맞게 바꿔야 하는건 당연한 것이다.

사용자 정의 operator new와 operator delete 역시 같다. new와 delete의 기본 요구사항은 뭐가 있을까?

 

 

new의 기본 요구사항

1.반환 값이 있어야 한다.

2.가용 메모리가 부족할 경우 new 처리자 함수를 호출해야 한다(49장 참고).

3.크기가 없는(0byte) 메모리 요청에 대한 대비책을 갖고 있어야 한다.

 

기본 요구사항 3가지를 비멤버 버전의 함수를 의사 코드로 구현해보자.

void* operator new(std::size_t size) throw(std::bad_alloc)
{
	using namespace std;

	if (size == 0)
		size = 1;

	while (true)
	{
		//size만큼 byte 할당

		if (/*할당 성공*/)
			return (/*할당된 메모리에 대한 포인터*/);

		//할당이 실패했을 경우 new 처리자 함수 호출
		new_handler globalHandler = set_new_handler(0);//현재 new 처리자를 NULL, 이전 처리자 반환
		set_new_handler(globalHandler);

		if (globalHandler)
			(*globalHandler)();
		else
			throw std::bad_alloc();
	}
}

요구하는 크기가 0byte 일 때, 1byte로 간주하고 처리 함으로써 요구사항 3을 해결하였고, 할당이 실패 했을 경우 new 처리자 함수를 호출 함으로써 요구사항 2을 해결했다.

 

위 코드는 싱글 스레드 환경에서는 사용할 법하지만, 멀티 스레드 환경에서는 new 처리자 함수를 둘러싼 자료구조들이 조작 될 때 스레드 안전성이 보장되어야 하기 때문에 스레드 잠금을 걸어야 한다.

operater new 함수에 무한 루프가 있다. 이 루프를 빠져나오는 조건은 메모리 할당이 성공하던지, new 처리자 함수 쪽에서 처리해야한다. new 처리자 함수는 가용 메모리를 늘려 주던가, 다른 new 처리자를 설치하든가, new 처리자를 제거하든가, bad_alloc 처럼 예외를 던지든지, 함수 복귀를 포기하고 도중 중단을 하는 방법 중 선택해야한다.

 

 

operator new의 상속 관계

상속 관계에 있을 때 operator new 사용을 생각해 봐야한다.

class Base
{
public:
	static void* operator new(std::size_t size) throw(std::bad_alloc);
};

class Derived : public Base { };

int main()
{
	Derived *p = new Derived; //Base::operator new 호출

	system("Pause");
	return 0;
}

만약 Base라는 클래스를 위한 operator new 함수가 있다면, 이 함수의 크기는 sizeof(Base)에 맞춰져 있다.

즉, 파생 클래스인 Derived클래스로 operator new를 호출한 경우 Base::operator new가 호출되는데 이때 byte 크기는 Base에 맞춰져 있어서 Dervied의 byte 크기는 알 수 없는 문제가 생긴다.

 

시작부분에서 메모리 크기를 확인한 후에 틀린 메모리 크기가 들어온 경우 표준 operator new를 호출하는 쪽으로 비껴가는 방법을 떠올릴 수 있다.

void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
	if (size != sizeof(Base))
		return ::operator new(size);
}

한가지 짚고 넘어가야 하는게 독립 구조(freestanding)의 객체는 반드시 크기가 0이 넘어야 한다는 사항이다(39장 참고).

이 규칙으로 인해 sizeof(Base)는 최소 1의 크기를 갖고 있다. 만약 매개변수 size가 0이 들어온 경우 조건문은 통과하지 못하므로 new의 기본 요구사항 3을 준수하고 있다.

 

사실 의문인게 파생 클래스인 경우 표준 operator new를 사용한다면 효율이 반감되는 느낌이 든다. 정적 멤버 함수기 때문에 가상 함수로도 못 만들고 방법이 없나 싶다. 나중에 알게 되면 정리하자.

 

 

delete의 기본 요구사항

1.NULL 포인터에 대한 delete 적용이 항상 안전해야 한다.

 

operator new에 비해 요구사항이 널널하다. 

기본 요구사항을 비멤버 버전의 함수를 의사 코드로 구현해보자.

void operator delete(void* rawMemory) throw()
{
	if (rawMemory == 0) //rawMemory가 NULL 포인터라면 함수 종료
		return;

	//rawMemory가 가리키는 메모리 해제
}

rawMemory가 NULL 포인터라면 바로 return 해줌으로써 요구사항 1을 해결하였다.

사실 void*에 대한 깨달음이 아직 없다. 좀 더 공부해보자.

 

 

operator delete의 상속 관계

operator new와 별차이 없다. 파생 클래스에 대한 처리를 준비해야하고, 이 처리는 표준 delete를 호출하면 된다.

class Base
{
public:
	static void operator delete(void* rawMemory, std::size_t size) throw(std::bad_alloc);
};

void Base::operator delete(void* rawMemory, std::size_t size) throw(std::bad_alloc)
{
	if (rawMemory == 0) //NULL 포인터 점검
		return;

	//삭제할 객체가 Base가 아닌경우(ex : 파생 클래스)
	if (size != sizeof(Base))
	{
		::operator delete(rawMemory);
		return;
	}

	//rawMemroy가 가르키는 메모리 해제

	return;
}

 

 

이것만은 잊지 말자

관례적으로 operator new 함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져가야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0byte에 대한 대책도 있어야 한다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰 메모리 블록에 대한 요구도 처리해야 한다.

operator delete 함수는 NULL 포인터가 들어왔을 때 아무 일도 하지 않아야 한다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 한다.

728x90

댓글