이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 370p ~ 381p
이펙티브 C++(370p ~ 381p)
요약
항목 52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자
- 다음과 같은 new 표현식을 썼을 때 호출되는 함수는 두 개이다.
operator new
호출Widget
의 기본 생성자 호출
Widget *pw = new Widget;
- 이 상황에서 첫 번째로 호출되는
operator new
호출은 무사히 지나갔는데, 두 번째로 호출되는Widget
의 기본 생성자를 호출하는 과정에서 예외가 발생했다고 가정해보자. - 이런 경우에는 첫 단계에서 이미 끝난 메모리 할당을 어떻게 해서든 취소하지 않으면 안 된다.
- 코드에서 이런 상황을 처리할 수 있는 방법이 없기 때문에 1단계의 메모리 할당을 안전하게 되돌리는 임무는 C++ 런타임 시스템이 담당한다.
- C++ 런타임 시스템이 담당하는 일은 1단계에서 자신이 호출한
operator new
함수와 짝이 되는 버전의operator delete
함수를 호출하는 것이다.- 이게 제대로 되려면
operator delete
함수들 중에서 어떤 것을 호출해야 하는지 런타임 시스템이 제대로 알고 있어야 된다. - 기본형
operator new
는 기본형operator delete
와 짝을 이룬다.
- 이게 제대로 되려면
// 기본형 new의 시그니처
void* operator new(std::size_t) throw(std::bad_alloc);
// 전역 유효범위에서의 기본형 delete 시그니처
void operator delete(void *rawMemory) throw();
// 클래스 유효범위에서의 기본형 delete 시그니처
void operator delete(void *rawMemory, std::size_t size) throw();
- 문제는 기본형이 아닌
operator new
를 선언할 때 발생한다. - 어떤 클래스에 대해 전용으로 쓰이는
operator new
를 만들고 있는데, 메모리 할당 정보를 로그로 기록해 줄 ostream을 지정받는 꼴오 만든다고 가정해보자. operator delete
는 기본형으로 만든다고 가정한다.
class Widget {
public:
// 비표준 형태의 operator new
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
// 클래스 전용 operator delete의 표준 형태
static void operator delete(void* pMemory, size_t size) throw();
}
operator new
함수는 기본형과 달리 매개변수를 추가로 받는 형태로도 선언할 수 있다.- 이런 형태의 함수를 위치지정 new라고 부른다.
- 위의 코드에 있는
operator new
는 위치지정 버전이다. - 위치지정 new는 개념적으로 그냥 추가 매개변수를 받는 new이기 때문에 가지각색일 수 있다.
- 그 중에서 유용한 버전이 있는데, 어떤 객체를 생성시킬 메모리 위치를 나타내는 포인터를 매개변수로 받는 것이다.
- 위의 코드에 있는
void* operator new(std::size_t, void* pMemory) throw();
- 이렇게 포인터를 추가로 받는 형태의 위치지정 new는 C++의 표준 라이브러리 일부로도 들어가 있다.(
#include <new>
만 하면 된다.)vector
의 경우 해당 벡터의 미사용 공간에 원소 객체를 생성할 때 이 위치지정 new를 쓰고 있다.- 매개변수를 추가로 받는 new를
위치지정 new
라고 부르게 된 것도vector
에서 처음으로 사용해서 그렇다. - 위치지정 delete 역시 이 개념에서 갈라져 나왔다.
- 다시
Widget
클래스를 보면 설계에 문제가 있다.
// operator new를 호출하는 데
// cerr을 ostream 인자로 넘기는데, 이때 Widget 생성자에서 예외가 발생하면 메모리 누출
Widget* pw = new (std::cerr) Widget;
- 메모리 할당은 성공했지만
Widget
생성자에서 예외가 발생한 경우,operator new
에서 할당한 메모리를 되돌리는 일은 C++ 런타임 시스템이 책임진다. - 런타임 시스템 쪽에는 호출된
operator new
가 어떻게 동작하는지 알아낼 방법이 없으므로 자신이 직접 할당을 되돌리는 것이 아니라 호출된operator new
와 대응되는operator delete
를 호출한다.- C++ 런타임 시스템은 매개변수의 개수 및 타입이 똑같은지 확인한다.
Widget
클래스의operator new
에는ostream&
타입의 매개변수를 추가로 받고 있으므로, 이와 짝을 이루는operator delete
가 마련되어 있어야 한다.
void operator delete(void* pMemory, std::ostream&) throw();
- 매개변수를 추가로 받아들인다는 면에서 위치지정 new와 비슷하므로 이런 형태의 delete를 가리켜
위치지정 delete
라고 부른다.- 위의
operator delete
가 없는Widget
클래스는 메모리 누수가 발생할 가능성이 있다.
- 위의
class Widget {
public:
// 비표준 형태의 operator new
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
// 클래스 전용 operator delete의 표준 형태
static void operator delete(void* pMemory, size_t size) throw();
// 비표준 형태의 new와 짝을 이루는 delete
static void operator delete(void* pMemory, std::ostream& logStream) throw();
}
- 이제는 다음 문장이 실행되다가
Widget
클래스의 생성자에서 예외가 발생하더라도 메모리 누출을 막을 수 있게 된다.
Widget* pw = new (std::cerr) Widget;
- 일반적인 경우처럼 사용자가 다음과 같이 직접 delete를 호출하면 기본형
operator delete
를 호출한다.- 위치지정 delete는 위치지정 new의 호출과 함께 호출되는 생성자에서 예외가 발생했을 때만 호출된다.
delete pw; // 기본형의 operator delete 호출
- 바깥쪽 유효범위에 있는 어떤 함수의 이름과 클래스 멤버 함수의 이름이 같으면 바깥쪽 유효범위의 함수가
이름만 같아도
가려진다. - 때문에 사용자가 쓸 수 있다고 생각하는 다른 new들을 클래스 전용의 new가 가리지 않도록 각별히 신경써야 한다.
class Base
{
public:
// 이 new가 표준 형태의 전역 new를 가림
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
}
// 에러
// 표준 형태의 전역 operator new가 가려짐
Base *pb = new Base;
// 문제없음
// Base의 위치지정 new가 호출
Base *pb = new (std::cerr)Base;
- 다음과 같은 파생 클래스는 전역
operator new
는 물론이고 자신이 상속받은 기본 클래스의operator new
까지 가린다.
class Derived: public Base {
public:
// 기본형 new를 클래스 전용으로 다시 선언
static void* operator new(std::size_t size)
throw(std::bad_alloc);
}
// 에러
// Base의 위치지정 new가 가려짐
Derived *pd = new (std::clog) Derived;
// 문제없음
// Derived의 operator new를 호출
Derived *pd = new Derived;
- 이러한 이름 가리기는 메모리 할당 함수를 작성하는 것만 신경 쓰면 큰 문제는 없다. C++가 전역 유효 범위에서 제공하는
operator new
의 표준 형태는 다음 세 가지이다.
// 기본형 new
void* operator new(std::size_t size) throw(std::bad_alloc);
// 위치지정 new
void* operator new(std::size_t size, void*) throw();
// 예외불가 new
void* operator new(std::size_t, const std::nothrow_t&) throw();
- 어떤 형태이든 간에
operator new
가 클래스 안에 선언되는 순간, 위의 표준 형태들이 몽땅 가려진다. - 사용자가 이들 표준 형태를 쓰지 못하게 막자는 것이 원래 의도가 아니라면, 사용자 정의
operator new
형태 외에 표준 형태들도 사용자가 접근할 수 있도록 열어 주도록 해야 한다. - 사용자 정의
operator new
를 만들었다면 이에 대응하는operator delete
도 만들어 줘야 한다. - 이를 좀 더 쉽게 사용하기 위해 기본 클래스를 하나 만들고, 이 안에 new 및 delete의 기본 형태를 전부 넣어둔다.
class StandardNewDeleteForms {
public:
// 기본형 new / delete
static void* operator new(std::size_t size) throw(std::bad_alloc)
{
return ::operator new(size);
}
static void delete(void *pMemory) throw()
{
::operator delete(pMemory);
}
// 위치지정 new / delete
static void* operator new(std::size_t size, void* ptr) throw()
{
return ::operator new(size, ptr);
}
static void delete(void *pMemory, void* ptr) throw()
{
::operator delete(pMemory, ptr);
}
// 예외불가 new / delete
static void* operator new(std::size_t size, std::nothrow_t& nt) throw()
{
return ::operator new(size, nt);
}
static void delete(void *pMemory, std::nothrow_t& nt) throw()
{
::operator delete(pMemory, nt);
}
}
- 표준 형태에 덧붙여 사용자 정의 형태를 추가하고 싶다면, 이제는 이 기본 클래스를 축으로 넓혀 가면 된다.
- 상속과 using 선언을 사용해서 표준 형태를 파생 클래스 쪽으로 끌어와 외부에서 사용할 수 있게 만든 후에, 원하는 사용자 정의 형태를 선언한다.
class Widget: public StandardNewDeleteForms
{
public:
// 표준 형태가 Widget 내부에 보이도록 만듬
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 사용자 정의 위치지정 new, delete 추가
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc)
{}
static void operator delete(void *pMemory, std::ostream& logStream)
throw()
{}
}
9. 그 밖의 이야기들
- 이 장은 최소한 소프트웨어가 제대로 동작하기를 원한다면 컴파일러 경고를 가볍게 넘기지 말 것을 강조하는 항목으로 시작한다.
- 그 다음에는 표준 C++ 라이브러리의 구성요소를 전반적으로 조명하는 항목이 나오는데, 새로이 도입된
TR1
의 주요 기능들도 여기서 다루어진다. - 마지막으로 셋째 항목에서는 부스트를 집중 조명한다.
항목 53: 컴파일러 경고를 지나치지 말자
- 누구나 한 번쯤은 저질러 봤을 법한 실수 하나를 써보았다.
class B {
public:
virtual void f() const;
}
class D: public B {
public:
virtual void f();
}
- 가상 함수인
B::f
를D::f
에서 오버라이드하겠다는 의도인데, 여기에 실수가 숨어 있다. - B 클래스의 f는 상수(const) 멤버 함수이지만, D 클래스의 f는 상수 멤버 함수가 아니다.
- 일부 컴파일러는 이 코드를 돌리면 다음과 같은 경고가 나온다.
warning: D::f() hides virtual B::f()
- 이것은 B에서 선언된 f가 D에서 오버라이드된 것이 아니라 아예 가려졌다는 것을 컴파일러가 목 놓아 외치고 있는 것이다.
- 주로 사용해 온 특정 컴파일러에서 내주는 경고 메시지들에 어느 정도 익숙해지고 나면, 이 외의 다른 메시지들도 어떤 뜻으로 나온 건지를 슬슬 이해하는 수준에 오게 된다.
- 가장 좋은 것은 최고 경고 수준을 걸더라도 경고 메시지 없이 컴파일되는 코드를 작성하는 것이다.
- 또한, 어떤 경고 메시지를 없애기 전에, 그 경고가 알려주려고 하는 바가 무엇인지 정확히 이해하는 것 역시 중요하다.
- 컴파일러 경고는 제작사의 고유 선택에 따라 달라지는 것이기 때문에, 이것만큼은 태생적으로 구현별 의존 사항이란 사실을 잊으면 안 된다.
메모
항목 52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자
operator new
함수의 위치지정 버전을 만들 때는, 이 함수와 짝을 이루는 위치지정 버전의operator delete
함수도 꼭 만들어야 한다. 이 일을 빼먹었다가는, 찾아내기도 힘들며 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험할 수 있다.new
및delete
의 위치지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의해야 한다.
항목 53: 컴파일러 경고를 지나치지 말자
- 컴파일러 경고를 쉽게 지나치치 말자. 여러분의 컴파일러에서 지원하는 최고 경고 수준에도 경고 메시지를 내지 않고 컴파일되는 코드를 만드는 쪽에 최선을 다하자.
- 컴파일러 경고에 너무 기대는 인생을 지양하자. 컴파일러마다 트집을 잡고 경고를 내는 부분이 천차만별이기 때문이다. 지금 코드를 다른 컴파일러로 이식하면서 익숙하던 경고 메시지가 온 데 간 데 없이 사라질 수도 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(381p ~ 392p) (0) | 2024.02.15 |
---|---|
이펙티브 C++(360p ~ 369p) (0) | 2024.02.13 |
이펙티브 C++(350p ~ 361p) (1) | 2024.02.12 |
이펙티브 C++(340p ~ 350p) (1) | 2024.02.10 |
이펙티브 C++(332p ~ 340p) (1) | 2024.02.09 |