일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 상속
- 다형성
- operator new
- virtual function
- 오블완
- Effective c++
- c++
- 영화 리뷰
- implicit conversion
- Smart Pointer
- effective stl
- 암시적 변환
- resource management class
- reference
- Vector
- 메타테이블
- 언리얼
- 티스토리챌린지
- lua
- 비교 함수 객체
- exception
- 반복자
- 스마트 포인터
- 예외
- 영화
- 루아
- more effective c++
- UE4
- 게임
- 참조자
Archives
- Today
- Total
스토리텔링 개발자
[More Effective C++] 27. 힙 전용, 힙 불가 클래스 만들기 본문
728x90
항목 27. 힙(heap)에만 생성되거나 힙에는 만들어지지 않는 특수한 클래스를 만드는 방법
객체가 힙에만 생성되게 하기
- 암시적 객체 생성 / 소멸(변수 선언에 의해 생성자가 자동으로 호출되고 유효범위의 종료에 의해 소멸자가 호출되는 것)을 불법화 시킨다.
- 생성자와 소멸자를 private로 선언한다.
- 생성자를 private화 하는 건 너무 과한 감이 있다. 만약 기존의 것에 적용한다면 인터페이스를 과하게 해친다.
- 그래도 생성자를 private로 하고자 한다면 큰 단점이 있는데..
- 너무 많은 생성자를 고려해야 한다는 점이다.
- 컴파일러가 자동 생성하는 생성자는 public이기 때문이다.
- 소멸자만 private로 선언한다.
- 유사 소멸자(소멸자 기능을 담는 함수)를 만들어서 객체를 소멸시킬 때는 이 유사 소멸자를 호출하도록 한다.
class UPNumber
{
public:
UPNumber();
UPNumber(int initValue);
UPNumber(double initValue);
UPNumber(const UPNumber& rhs);
// 유사 소멸자
// const인 이유는, 상수 객체도 소멸될 수 있기 때문이다.
void destroy() const { delete this; }
...
private:
~UPNumber(); // 소멸자 private 선언
};
UPNumber n; // 에러! 암시적으로 소멸자가 호출될 때 불법이 된다.
UPNumber* p = new UPNumber; // 성공
...
delete p; // 에러! 소멸자 호출 불가
p->destroy(); // 성공
- 생성자나 소멸자의 접근을 제한하는 방법은 다 좋지만, 클래스 상속과 합성이 불가능하다는 문제가 있다. (항목 26 참조)
// 상속의 경우
// 에러! 소멸자나 생성자가 컴파일되지 않는다.
class NonNegativeUPNumber : public UPNumber
{ ... };
// 합성의 경우
class Asset
{
private:
// 에러! 소멸자나 생성자가 컴파일되지 않는다.
UPNumber value;
...
};
class UPNumber
{
protected:
~UPNumber() { ... }
}
// 상속의 경우
// 문제 없음.
// 하지만 힙에 생성되었다는 조건을 만족시켰다고 할 수 있을까?
class NonNegativeUPNumber : public UPNumber { ... };
// 합성의 경우
class Asset
{
public:
Asset(int initValue) : value(new UPNumber(initValue)) { ... }
~Asset() { value->destroy(); }
...
private:
UPNumber* value; // 포인터. 문제 없음.
};
어떤 객체가 힙에 생성되었는지 확인하기
- 위 코드에서 NonNegativeUpNumber은 문제 소지가 있다.
NonNegativeUPNumber n; // 컴파일 에러가 나지 않는다.
// 하지만 이 경우 UPNumber 부분은 힙에 생성되지 않았다.
- 결국 UPNumber의 파생 클래스를 모두 힙에 생성되도록 제약을 가하고 싶다면 어떻게 해야 할까?
- 생성자에서 힙 기반인지 아닌지 알아낼 수 있으면 쉽겠지만..
- 아쉽게도 그런 방법은 없다.
- 그래도 operator new를 재정의해서 잘 만지면 가능할 거 같은데?
class UPNumber
{
public:
class HeapConstraintViolation {}; // 힙 기반이 아닐 때를 위한 예외
// operator new 재정의
static void* operator new(size_t size);
UPNumber();
...
private:
static bool onTheHeap; // 힙인지 아닌지에 대한 플래그
};
bool UPNumber::onTheHeap = false;
void* UPNumber::operator new(sizt_t size)
{
// 힙에 메모리를 마련할 때 플래그를 켠다.
onTheHeap = true;
return ::operator new(size);
}
UPNumber::UPNumber()
{
if(!onTheHeap) // 메모리 할당이 없었으면 예외 발생
throw HeapContraintViolation();
...
onTheHeap = false; // 플래그 초기화
}
- 문제점
- 배열 메모리는 operator new[]에 의해 할당된다.
- operator new[] 도 재정의하면 된다고 생각할 수 있다. 하지만..
- 배열 메모리는 생성자는 배열 크기만큼 호출되지만, operator new[]는 한번만 호출된다.
- onTheHeap은 한번만 활성화되지만, 생성자는 여러번 호출되므로 논리적으로 오류.
- 배열만 아니라면 문제가 없을까? 아쉽게도 아니다.
- 할당이 한 문장에서 일어나면 operator new의 호출 순서를 보장할 수 없다.
-
// 이렇게 호출될 경우 UPNumber* pn = new UPNumber(*new UPNumber); // 이렇게 될 걸 예상하겠지만.. // 1. 제일 왼쪽의 것의 operator new가 호출 // 2. 제일 왼쪽의 것의 생성자가 호출 // 3. 다음 것의 operator new가 호출 // 4. 다음 것의 생성자가 호출 // 아쉽게도 실제로는 컴파일러에 따라 다른 사항이다. // C++ 언어 스펙에 규정이 없기 때문이다. // 즉, 다음처럼 될 여지가 있다! // 1. 제일 왼쪽의 것의 operator new가 호출 // 2. 다음 것의 operator new가 호출 // 3. 제일 왼쪽의 것의 생성자가 호출 // 4. 다음 것의 생성자가 호출
-
- 배열 메모리는 operator new[]에 의해 할당된다.
- 이식성을 무시한 방법
- 대부분의 시스템이 프로그램의 주소 공간을 연속된 주소로 배열한다는 사실을 활용한다면?
- 위의 그림대로라고 가정한다면, 메모리가 힙 영역에 있는지를 알아낼 수 있지 않을까?
// 해당 주소값이 힙 영역인지 알아내는 함수
// 바람직하지는 않다..
bool onHeap(const void* address)
{
char onTheStack; // 스택에 지역 변수를 만들어 보고
return address < &onTheStack; // 주소값 위치를 비교
}
- 문제점
- 객체가 생성되는 장소는 세 가지이다.
- 힙 / 스택 / 정적 객체
- 정적 객체는 전역 객체와 네임스페이스 안에 정의된 객체까지 모두 포함한다.
- 보통 위와 같은 메모리 시스템이라고 가정한다면, 정적 객체는 힙의 아래쪽에 둔다.
- 그렇다면 onHeap으로 정적 객체도 체크되는 버그가 있다고 볼 수 있다.
- 객체가 생성되는 장소는 세 가지이다.
char* pc = new char;
onHeap(pc); // true 반환
char c;
onHeap(c); // false 반환
static char sc;
onHeap(sc); // true 반환
- 힙에 할당되는지 알아내는 이식성 있는 방법은 없다. 포기하자.
- 만약 힙 영역에 있는지 알고 싶은 이유가, delete를 해도 괜찮은지 알고 싶기 때문이라면..
- 이 두 가지는 완전히 동일한 의미가 아니라는 걸 상기하자.
- 왜냐하면 힙에 있다고 해서 100% 안전하게 삭제할 수 있는 것은 아니기 때문이다.
class Asset
{
private:
UPNumber value;
...
};
// pa는 힙에 생성된다.
Asset* pa = new Asset;
// 그러므로 pa->value 역시 힙에 있다.
// 하지만 이건 재앙이다.
delete &pa->value;
- 다행히 어떤 포인터가 삭제해도 괜찮은 포인터인지 알아내는 방법은 훨씬 간단하다.
- operator new에서 지금까지 할당된 주소들의 콜렉션을 유지하면 된다.
std::unordered_set<void*> heap_allocations;
void* operator new(size_t size)
{
void* ptr = malloc(size);
// 콜렉션에 저장
heap_allocations.insert(ptr);
return ptr;
}
void operator delete(void* ptr) noexcept
{
// 콜렉션에서 제거
heap_allocations.erase(ptr);
free(ptr);
}
bool isSafeToDelete(const void* ptr)
{
// 콜렉션에 존재하는지 체크
return heap_allocations.find(ptr) != heap_allocations.end();
}
- 문제점
- 전역 함수는 객체지향적으로 볼땐 아무래도 껄끄럽다.
- operator new와 operator delete를 또 새로 구현한 다른 소프트웨어와 함께 작동할 수 없게 된다.
- 힙 할당을 할 때마다 콜렉션을 업데이트하는 것은 효율적이진 않다.
- isSafeToDelete 함수를 모든 경우에 대해 동작하도록 구현하는 것은 불가능하다.
- 다중 상속으로 만들어진 객체나 가상 기본 클래스로부터 만들어진 객체는 주소도 여러개이다.
- operator new에서 반환된 주소가 isSafeToDelete로 넘어간 주소가 같으리라는 보장이 없다.
- 전역 함수는 객체지향적으로 볼땐 아무래도 껄끄럽다.
- 위의 기능을 유지하되, 문제점들만 해결할 방법이 있다.
- 추상 믹스인 기본 클래스(abstract mixin base class)
- 믹스인(mix in) 클래스
- 명확한 기능을 딱 하나만 제공하는 클래스이다.
- 이 클래스의 파생 클래스가 제공할지도 모르는 다른 기능과 호환되도록 설계된다.
- 믹스인(mix in) 클래스
class HeapTracked
{
public:
class MissingAddress {}; // 예외
virtual ~HeapTracked() = 0;
static void* operator new(size_t size);
static void operator delete(void* ptr);
bool isOnHeap() const;
private:
typedef const void* RawAddress;
static list<RawAddress> addresses; // 할당된 포인터를 컬렉션으로 관리
};
list<RawAddress> HeapTracked::addresses;
HeapTracked::~HeapTracked() {}
void* HeapTracked::operator new(size_t size)
{
void* memPtr = ::operator new(size):
address.push_front(mempPtr); // 메모리 할당 시 컬렉션에 추가
return memPtr;
}
void HeapTracked::operator delete(void* ptr)
{
list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr);
if(it != addresses.end())
{
addresses.erase(it); // 컬렉션에 있으면 삭제 후 정상 동작
::operator delete(ptr);
}
else
{
throw MissingAddress(); // 예외 발생
}
}
bool HeapTracked::isOnHeap() const
{
// 다중 상속 ,가상 상속을 통해 만들어진 객체는 여러개의 주소를 가진다.
// 하지만 이 함수는 HeapTracked 객체에 대해서만 적용되기 때문에
// dynamic_cast를 통해 해결해 볼 수 있다.
// - const void*로 캐스트하여 이 객체의 가장 앞 부분 포인터를 얻어온다.
// dynamic_cast는 가상 함수를 적어도 하나 가진 클래스에 대해서만 적용 가능하므로
// 전역에서는 불가능한 사용 방법이다.
const void* rawAddress = dynamic_cast<const void*>(this);
// 컬렉션에서 찾는다.
list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr);
return it != addresses.end();
}
// 사용
class Asset : public HeapTracked
{
private:
UPNumber value;
...
};
Asset* ap;
...
if(ap->isOnHeap())
// ap는 힙 기반 객체다.
else
// ap는 힙 기반 객체가 아니다.
객체가 힙에 생성되지 않게 하기
- 고려해야 하는 경우의 수
- 객체가 직접 인스턴스화 되는 경우
- 파생 클래스의 기본 클래스 부분으로 들어가는 경우(상속)
- 다른 객체의 멤버로 들어가는 경우(합성)
- 객체가 직접 인스턴스화 되는 경우
- operator new를 접근 제한한다.
class UPNumber
{
private:
static void* operator new(size_t size);
// 특별한 이유가 없으면 delete도 함께 같은 성질로 선언한다.
static void operator delete(void* ptr);
...
}
// 물론 operator new[]와 operator delete[] 역시 private 선언해야 할 것이다.
- 파생 클래스의 기본 클래스 부분으로 들어가는 경우(상속)
- 이는 첫 번째를 해결하면 자연스럽게 해결된다.
- 왜냐하면 operator new와 operator dlete는 상속이 가능한 함수이기 때문이다.
- 파생 클래스에서 억지로 public으로 다시 선언하지 않는 한 기본 버전의 것이 상속된다.
class NonNegativeUPNumber : public UPNumber { ... };
NonNegativeUPNumber n1; // 문제 없음
static NonNegativeUPNumber n2; // 문제 없음
NonNegativeUPNumber* p = new NonNegativeUPNumber; // 컴파일 에러!
- 다른 객체의 멤버로 들어가는 경우(합성)
- 이 경우는 걸러지지 않는다.
- 이를 거르기 위한 이식성 있는 방법은 없다.
// 이 경우는 힙 공간에 할당되어 있지만 문제 없다.
class Asset
{
public:
Asset(int initValue);
...
private:
UPNumber value;
};
Asset* pa = new Asset(100); // 문제 없음
728x90
'개발 > More Effective C++' 카테고리의 다른 글
[More Effective C++] 29. 참조 카운팅 (0) | 2024.09.27 |
---|---|
[More Effective C++] 28. 스마트 포인터 (0) | 2024.09.09 |
[More Effective C++] 26. 클래스 인스턴스 개수 제한 (0) | 2024.09.05 |
[More Effective C++] 25. 함수를 가상 함수처럼 만들기 (4) | 2024.09.04 |
[More Effective C++] 24. 다형성의 비용 (0) | 2024.09.03 |
Comments