람다 표현식

 

함수의 익명 표기법이다.

일급 함수 및 순수 함수를 만들 때 용이하다.

 

람다식은 크게 캡처 목록 [], 매개변수 목록 (), 본문 {} 3가지로 구성되어있다.

 

간단한 함수를 람다로 표현하기

한 번만 실행되는 한 줄짜리 간단한 함수의 경우 클래스의 멤버 함수나 전역 함수로 만들기보다는 사용되는 곳에 직접 정의하면 가독성 향상 및 최적화될 가능성이 높아진다.

 

std::for_each(std::begin(vehicles), std::end(vehicles), PrintOut); /* 기존 방법 */

std::for_each(std::begin(vehicles),
              std::end(vehicles),
              [](const Vehicle &vehicle) {
                  std::cout << vehicle.GetType << '\n';
              });

 

 

여러 줄의 함수를 람다로 표현하기

한줄 뿐만 아니라 여러줄의 함수도 람다 표현식으로 만들 수 있다.

 

std::for_each(std::begin(vect),
              std::end(vect),
              [](int n) {
                  std::cout << n << " is";
                  if (n < 2) {
                      if (n == 0) std::cout << " not";
                  }
                  else {
                      for (int j = 2; j < n; ++j) {
                          if (n % j == 0) {
                              std::cout << " not";
                              break;
                          }
                      }
                  }
                  std::cout << " prime number" << '\n';
              });

 

 

람다 표현식에서 값 반환

std::transform(std::begin(vect2),
               std::end(vect2),
               begin(vect3),
               [](int n) -> double {
                   return n / 2.0;
               });

 

람다 표현식에서 값 반환시 타입 명시가 필요하다면 매개 변수와 본문 사이에 명시해주면 된다.

명시하지 않으면 컴파일러가 추정한다.

 

 

람다 표현식에서 값 캡처하기

[=](int){ ... }; // 외부 변수 값 복사
[&](int){ ... }; // 외부 변수 참조
[=, &a](int){ ... }; // 외부 변수 값 복사. 단, a만 참조

 

외부 변수를 값 복사로 캡처하면 const화 되기때문에 수정이 불가능하다.

하지만 값을 수정하되 외부에 영향을 미치지 않고 싶다면(call by value) mutable 키워드를 추가하면 된다.

 

int a = 1;
int b = 2;

std::for_each(std::begin(vect),
              std::end(vect),
              [=](int& x) mutable {
                  int old = a;
                  a = b;
                  b = old;
              });

 

덧붙여서 [=], [&] 같은 암시적 캡처의 사용은 절대적으로 지양하는것이 좋다. 필요한 것만 명시적으로 캡처해야한다.

 

 

초기화 캡처

C++14부터 추가된 기능이다.

외부 변수의 값을 캡처해서 람다 표현식의 변수에 할당할 수 있다.

 

int a = 5;
auto myLambda = [&x = a]() { x += 2; }; /* 람다에서 사용하는 x에 a값을 대입한다 */
myLambda();
std::cout << a << '\n'; /* 람다식에 의해 2가 증가했으므로 7이 출력된다 */

 

외부 변수 참조 캡처와 같아보이지만 복사가 불가능한 unique_ptr같은 이동 전용 변수도 캡처하여 람다에서 사용할 수 있다.

 

pNums = std::make_unique<std::vector<int>>(nums);
auto a = [ptr = std::move(pNums)]() { ... }; /* 단순히 pNums를 값으로 캡처하면 오류가 발생한다 */

 

위와 같은 경우는 캡처될 때 move가 사용되었으므로 pNums는 empty가 된다.

 

제너릭 람다 표현식

C++14부터 람다 표현식의 매개변수에 auto 타입이 사용가능해졌다.

 

auto findMax = [](auto &x, auto &y) { return x > y ? x : y; };

 

 

추가로 C++17부터는 캡처 목록에 *this를 사용해서 객체의 복사본을 캡처할 수 있다.

또한 람다 표현식으로 constexpr 객체를 컴파일 타임에 생성할 수 있다.

 

constexpr

객체나 함수 앞에 붙일 수 있는 키워드. 해당 객체나 함수의 반환값을 컴파일 타임에 알 수 있다는 의미를 명시한다.

 

C++ 표준 라이브러리 구현 살펴보기

 

컨테이너 안에 객체 배치하기

std::array는 C++11에 새로 추가된 컨테이너이다.

vector는 가변 배열, array는 고정 배열이라는 차이가 있다.

 

std::array<int, 10> a = { 0, 1, 2, .. };
std::vector<int> v = { 0, 1, 2, .. };

 

둘 다 인덱스로 컨테이너의 특정 요소에 접근할 때는 [] 연산자 대신 at 함수를 사용하는 것이 안전하다.

인덱스가 컨테이너 범위를 벗어날 때, at 함수는 out_of_range 예외를 던지는데 [] 연산자는 아무것도 하지 않기 때문에 미정의 동작이 발생할 수 있다.

 

 

알고리즘 사용하기

배열이나 벡터에 저장된 요소는 표준 라이브러리를 사용하여 정렬이나 특정 값을 찾을 수 있다.

 

◾ 정렬

/* 정렬 */
bool comparer(int a, int b)
{
    return a > b;
}

std::vector<int> vect = { 20, 43, 11, 78, 5, 96 };

std::sort(std::begin(vect), std::end(vect)); /* 기본은 오름차순 정렬 */
std::sort(std::begin(vect), std::end(vect), comparer); /* comparer를 이용해서 내림차순 정렬 */

 

3번째 인자로 비교 함수를 넘겨주면 임의의 조건으로 정렬시킬 수 있다.

 

◾ 탐색

bool TwoWheeled(const Vehicle &vehicle)
{
    return vehicle.GetNumOfWheel() == 2 ? true : false;
}

std::vector<Vehicle> vehicles;
...
auto tw = find_if(std::begin(vehicles), std::end(vehicles), TwoWheeled); // TwoWheeled : 탐색 조건
auto ntw = find_if_notstd::begin(vehicles), std::end(vehicles), TwoWheeled);

 

find_if는 탐색 조건에 부합하는 인자들을 반환하고 find_if_not은 탐색 조건에 부합하는 인자들을 제외하고 반환한다.

 

◾ 순회(루프)

void PrintOut(const Vehicle &vehicle)
{
    std::cout << vehicle.GetType() << '\n';
}

vector<Vehicle> vehicles;
...
std::for_each(std::begin(vehicles), std::end(vehicles), PrintOut);

 

루프를 돌며 컨테이너의 요소를 인자로 넘겨서 필요한 함수를 실행시킬 수도 있다.

모던 C++의 새로운 기능 익히기

 

auto 키워드로 데이터 타입을 자동으로 정의하기

auto a = 1; // int
auto b = 1.0; // double

auto add(int i, int j) // 반환 타입 추론
{
    return i + j;
}

auto add(int j, int j) -> int // 후행 반환 타입
{
    return i + j;
}

 

초기화에 선언된 변수의 실제 타입을 "컴파일 타임"에 추론하기 위해 사용한다.

변수 뿐만 아니라 함수의 반환 타입도 자동으로 추론한다.

반환타입에 auto를 사용하더라도 반환 타입을 지정할수도 있다.

 

함수의 매개변수에 auto 타입을 사용할 수 없지만 람다 함수의 매개변수에는 사용이 가능하다. (클로저는 런타임에 생성되기 때문에 가능한 것으로 보인다.)

 

auto 타입 반환은 C++11부터 가능해졌다.

덧붙여서 C++11에서의 auto 타입 반환에는 반드시 후행 반환 타입이 지정되어야 했지만 C++14부터는 생략이 가능해졌다.

 

 

decltype 키워드로 표현식 타입 질의하기

decltype(declared type, 선언된 형식)은 객체나 표현식의 타입을 "컴파일 타임"에 추출하고 싶을 때 사용한다.

 

const int func1();
const int& func2();
int i;

struct X { double d; };
const X* x = new X();

decltype(func1()) f1; // func1()의 반환 타입을 사용해서 const int f1 선언
decltype(func2()) f2; // func2()의 반환 타입을 사용해서 const int& f2 선언
decltype(i) i1; // i의 타입을 사용해서 int i1 선언

decltype(x->d) d1; // x의 멤버변수 d의 타입을 사용해서 double d1 선언
decltype((x->d)) d2; // 표현식 x->d의 주소 타입을 사용해서 const double& d2 선언

 

auto와 decltype을 조합하면 간결하게 코드를 작성할 수 있다.

 

template<typename I, typename J>
auto add(I i, J j) -> decltype(i + j) // decltype(auto) add(I i, J j) 같은 의미이다
{
    return i + j;
}

 

만약 반환 타입이 auto임에도 후행 반환 타입을 지정하지 않았다면 의도하지 않은 결과가 나올 수 있다.

후행 반환 타입을 지정하지 않는다면 반환 타입을 auto 대신 decltype(auto)로 하는것이 안전하고 간결해진다.

 

참고로 decltype((x->d))처럼 표현식이 lvalue라면 똑같이 lvalue 참조로 보고된다.

 

 

null 포인터

과거부터 사용되던 NULL 매크로를 대체하기 위해 만들어졌다. 모호함이 해결된다.

 

void funct(const char *);
void funct(int);

funct(NULL); // 널 포인터인지 0인지 모호해진다
funct(nullptr); // 확실하게 널 포인터이다

 

 

비멤버 함수 begin()과 end()

본래 begin과 end는 각 컨테이너에서 제공되는 멤버 함수였다. 그래서 배열같은 경우는 인덱스를 이용해서 순회해야 했다.

하지만 C++11부터 비멤버 함수로도 제공된다.

 

int arr[] = {0, 1, 2, ... };
for(unsigned int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i) // 과거 스타일의 배열 순회
for(auto i = std::begin(arr); i != std::end(arr); ++i) // 비멤버 begin, end 사용

 

더 이상 배열의 길이를 신경쓰지 않아도 되기 때문에 코드를 간결하게 작성할 수 있다.

 

std::vector<int> v;
auto iter1 = v.begin();
auto iter2 = std::begin(v);

 

당연히 둘은 동일한 타입으로 추론된다.

 

 

범위 기반 for 루프로 컬렉션 내 요소 순회

모던 C++은 컬렉션 내 요소를 순회하기 위해 범위 기반 for 루프를 제공한다.

 

int arr[] = { 0, 1, 2, .. };
for(auto a : arr)

53. 컴파일러 경고를 지나치지 말자

컴파일러가 내뱉는 경고 메시지를 없애기 전에 그 경고가 무엇을 알리려는지를 정확히 이해해야 한다.

 

◾ 컴파일러 경고를 쉽게 지나치지 맙시다. 여러분의 컴파일러에서 지원하는 최고 경고 수준에도 경고 메시지를 내지 않고 컴파일되는 코드를 만드는 쪽에 전력을 다 하십시오.

◾ 컴파일러 경고에 너무 기대는 인생을 지양하십시오. 컴파일러마다 트집을 잡고 경고를 내는 부분들이 천차만별이기 때문입니다. 지금 코드를 다른 컴파일러로 이식하면서 여러분이 익숙해져 있는 경고 메시지가 온 데 간 데 없이 사라질 수도 있습니다.

 

 

54. TR1을 포함한 표준 라이브러리 구성요소와 편안한 친구가 되자

(참고: 책에 기재된 TR1의 기능은 모두 모던 C++에 흡수되었다.)

◾ 스마트 포인터

◾ tr1::function

◾ tr1::bind

◾ 해시 테이블

◾ 정규 표현식

◾ 튜플

◾  tr1::array

◾ tr1::mem_fn

◾ tr1::reference_wrapper

◾ 난수 발생

◾ 특수 용도의 수학 함수

◾ C99 호환성 확장 기능

◾ 타입 특성정보(type trais)

◾ tr1::result_of

 

◾ 최초에 상정된 표준 C++ 라이브러리의 주요 구성요소는 STL, iostream, 로케일 등입니다. 여기에는 C89의 표준 라이브러리도 포함되어 있습니다.

◾ TR1이 도입되면서 추가된 것은 스마트 포인터, 일반화 함수 포인터(std::function), 해시 기반 컨테이너, 정규 표현식 그리고 그 외의 10개 구성요소입니다.

◾ TR1 자체는 단순히 명세서일 뿐입니다. TR1의 기능을 사용하기 위해서는 명세를 구현한 코드드 구해야 합니다. TR1 구현을 구할 수 있는 자료처 중 한 군데가 바로 부스트입니다.

 

 

55. Boo子有親! 부스트를 늘 여러분 가까이에

(참고: 본문에서 언급하는 수준의 Boost 라이브러리 역시 TR1을 구현한 것이기 때문에 모던 C++에 대부분 흡수된것으로 보인다. 예를들면 람다식.)

 

◾ 문자열 및 텍스트 처리

◾ 컨테이너

◾ 함수 객체 및 고차 프로그래밍

◾ 일반화 프로그래밍

◾ 템플릿 메타프로그래밍

◾ 수학 및 수치 조작

◾ 정확성 유지 및 테스트

◾ 자료구조

◾ 타 언어와의 연동 지원

◾ 메모리

◾ 기타

 

◾ 부스트는 동료 심사를 거쳐 등록되고 무료로 배포되는 오픈 소스 C++ 라이브러리를 개발하는 모임이자 웹사이트입니다. 또한 C++ 표준화에 있어서 영향력 있는 역할을 맡고 있습니다.

◾ 부스트에서 배포되는 라이브러리들 중엔 TR1 구성요소에 들어간 것도 있지만, 그 외에 다른 라이브러리들도 아주 많습니다.

'도서 > Effective C++' 카테고리의 다른 글

8. new와 delete를 내 맘대로  (0) 2022.12.07
7. 템플릿과 일반화 프로그래밍  (0) 2022.12.05
6. 상속, 그리고 객체 지향 설계  (0) 2022.12.04
5. 구현  (0) 2022.12.03
4. 설계 및 선언  (0) 2022.12.01

49. new 처리자의 동작 원리를 제대로 이해하자

할당할 메모리가 없어서 operator new 함수가 동작하지 않을 때, operator new 함수는 예외를 던진다.

예외를 던지기 전에 사용자가 에러 처리를 할 수 있도록 new 처리자(핸들러)를 먼저 호출하게 된다.

 

표준 라이브러리에서 set_new_handler라는 함수를 제공한다.

 

 

namespace std {
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

 

새로운 new 핸들러를 인자로 받고 기존 new 핸들러를 반환시켜준다.

operator new 함수가 예외를 던지면 충분한 메모리를 찾아낼 때까지 new 핸들러를 반복해서 호출하게 된다.

이 때, new 핸들러가 프로그램 동작에 있어서 긍정적인 영향을 주게끔 설계했다면 아래의 동작 중 한가지를 반드시 수행해주어야 한다.

 

◾ 사용할 수 있는 메모리를 더 많이 확보한다

메모리 풀을 사용하는 방식이 이에 해당할 수 있다. 프로그램 시작 시, 메모리 블록을 미리 크게 잡아놓고 new 핸들러가 호출되면 메모리 블록을 사용할 수 있게 허용하는 방식이다.

 

◾ 다른 new 처리자를 설치한다

현재의 new 핸들러가 문제를 처리할 수 없으면 다음부터 다른 new 핸들러를 호출할 수 있도록 set_new_handler 함수를 통해 새로운 new 핸들러를 설정한다.

 

◾ new 처리자의 설치를 제거한다

set_new_handler 함수에 널 포인터를 넘김으로써 operator new 함수가 예외를 던지게 만든다.

 

◾ 예외를 던진다

bad_alloc이나 거기서 파생된 타입의 예외는 operator new 함수에서 처리하지 못하기 때문에 new 핸들러 안에서 던지게 만든다.

 

◾ 복귀하지 않는다

abort나 exit 함수를 호출하여 프로그램을 종료시킨다.

 

클래스 별로 new 핸들러를 다르게 제공하고 싶다면 클래스 내부에서 operator new와 new 핸들러를 정의하면 된다.

 

 

class NewHandlerHolder {
public:
    explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {} // 현재 new 핸들러 보관
    ~NewHandlerHolder() { std::set_new_handler(handler); } // 보관한 new 핸들러로 복구
private:
    std::new_handler handler;
    
    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& operator=(const NewHandlerHolder&); // 복사 방지
};


class Widget {
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
    static std::new_handler currentHandler; // 현재 new 핸들러
};

std::new_handler Widget::currentHandler = 0;

std::new_handler Widget::set_new_handler(std::new_handler) throw()
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}    

void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
    NewHandlerHolder h(std::set_new_handler(currentHandler)); // 기존 new 핸들러 저장
    
    return ::operator new(size); // 할당 실패시 currentHandler 호출
} // 자원 관리 객체에 의해 이전의 전역 new 핸들러로 자동 복원

void outOfMem(); // new 핸들러가 가리킬 함수

Widget::set_new_handler(outOfMem); // new 핸들러 교체
Widget *pw1 = new Widget;

 

Widget 객체를 동적 할당하기 전에 new 핸들러를 지정해야하고(지정되지 않으면 널 포인터이므로 바로 예외를 던진다), 자원 관리 객체에 의해 Widget::operator new 호출이 종료되는 시점에 자동으로 기존의 new 핸들러로 복원시킨다.

 

자원 관리 객체를 이용한 할당에러 처리를 구현하는 코드는 어느 클래스에서 구현하더라도 똑같이 나올것이다.

그래서 할당에러 처리 기능만 따로 기본 클래스 템플릿으로 만들고 할당에러 처리가 필요한 클래스들이 상속 받아서 사용하면 훨씬 더 깔끔해진다.

 

template<typename T>
class NewHandlerSupport {
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ... // 필요하면 operator new의 다른 버전들도 추가
private:
    static std::new_handler currentHandler;
};

template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
    NewHandlerHolder h(std:;set_new_handler(currentHandler));
    return ::operator new(size);
}

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;


class Widget: public NewHandlerSupport<Widget> { ... };

 

Widget 클래스에서는 더 이상 set_new_handler 및 operator new 함수를 선언할 필요가 없다.

그리고 특이하게도 NewHandlerSupport 클래스 템플릿은 템플릿 매개변수를 받음에도 불구하고 매개변수를 전혀 사용하지 않는다. 그저 정적 데이터 멤버인 std::new_handler의 사본을 만들기 위해서 존재한다.

참고로 이 패턴을 CRTP라고 부른다. 정적 다형성 및 메타프로그래밍에 사용되는 패턴이다.

대신 CRTP를 사용하면 다중 상속이 어쩔 수 없이 끌려나올 수 있다.

 

마지막으로 일반적인 new를 사용하든, 예외불가 new를 사용하든 어쨌거나 new 처리자는 양쪽에서 모두 쓰인다.

 

◾ set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있습니다.

◾ 예외불가(nothrow) new는 영향력이 제한되어 있습니다. 메모리 할당 자체에만 적용되기 때문입니다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있습니다.

 

 

50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자

new 와 delete를 바꾸는 일반적인 세 가지 이유

 

◾ 잘못된 힙 사용을 탐지하기 위해

요구하는 크기보다 약간 더 크게 할당해서 탐지용 비트를 추가하여 메모리 블록에 잘못 접근했을 경우 로그를 남긴다.

 

 

◾ 효율을 향상시키기 위해

메모리 단편화 등의 문제를 방지하기 위해. new/delete는 일반적으로 무난하게 동작하지만 프로그램에 따라 사용자 정의 버전의 성능이 더 좋을수도 있다.

 

◾ 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해

 

물론 꼭 만들어 써야 할 필요가 없다면 굳이 만들 필요가 없다. 만들게 되더라도 오픈 소스 라이브러리를 보는것이 좋다.

그리고 new 처리자 함수를 호출하는 루프가 반드시 있어야 하는것이 관례이다.

 

◾ 개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 나름대로 타당한 이유가 있습니다. 여기에는 수행 성능을 향상시키려는 목적, 힙 사용 시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함됩니다.

 

 

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

operator new 함수를 구현할 때의 기본적인 요구사항들이 있다.

 

◾ 값 반환

◾ 할당 실패 시, new 처리자 호출 및 0바이트 메모리 할당 요청에 대한 대비

◾ 기본 형태의 new가 가려지지 않게 함

 

operator new는 메모리 할당이 성공할 때까지 new 처리자를 호출하게끔 무한루프로 구현하는 것이 관례이다.

루프를 빠져나올 수 있는 조건은 메모리 할당에 성공하거나 new 처리자 함수가 항목 49에서 언급한 다섯가지 조건 중 한가지를 만족시켜야 한다.

 

주의해야 할 점으로는 operator new 역시 파생 클래스로 상속되는 함수라는 점이다.

 

class Base {
public:
    static void* operator new(...);
};
class Derived: public Base {};

Derived *p = new Derived; // Base::operator new 호출

 

Base의 operator new는 Base의 크기에 맞는 메모리를 할당하게 될텐데, Derived가 상속받아 호출하게 되면 크기가 맞지 않게될 수 있다.

이런 경우 틀린 메모리 크기가 들어왔을 때, 표준 operator new를 호출하도록 우회시키면 된다.

 

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

 

추가로 operator new[]를 직접 구현하게 된다면 원시 메모리의 덩어리를 할당하는 작업만 해야한다.

 

 

operator delete의 구현 관례는 매우매우 단순하다. 널 포인터에 대한 delete 적용이 안전하다는 보장만 지켜주면 된다.

틀린 메모리 크기에 대한 우회를 구현했다면 delete에서도 동일하게 처리해주면 된다.

 

마지막으로 가상 소멸자가 없는 파생 클래스의 객체를 삭제하려는 경우, operator delete의 매개변수인 size_t의 이상할 수도 있다.

 

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

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

 

 

52. 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자

new 표현식을 쓰게되면 우선 operator new 호출로 메모리가 할당되고 이후 생성자가 호출되는 구조를 가지게 되어있다.

두개의 함수가 호출되는 것이다.

 

Widget *pw = new Widget;

 

만약 operator new 호출로 메모리 할당에는 성공했지만 생성자 호출 도중에 예외가 발생하면 할당된 메모리를 취소시켜야 한다. 하지만 생성자에서 예외가 발생했기 때문에 pw에 포인터가 대입되는 일은 생기지 않는다.

메모리는 할당되었지만 포인터를 돌려받지 못했기 때문에 사용자 입장에서는 해당 메모리에 접근할 방법이 아예 없다.

그래서 런타임 시스템이 할당된 메모리를 대신 반환해준다.

 

이 때 런타임 시스템은 호출된 operator new와 짝이 되는 operator delete를 호출하게 되는데, 이건 사용자가 준비해두어야 한다.

 

void* operator new(std::size_t, std::ostream&) throw(std::bad_alloc); // 위치지정 new
void operator delete(void *, std::ostream&) throw(); // 위치지정 delete

 

new-delete의 짝이 맞지 않는다면 생성자에서 예외 발생 시, 런타임 시스템은 아무런 operator delete도 실행하지 않는다.

또한 new의 호출이 예외 없이 잘 이루어졌다면 추후 delete 호출 시 절대로 위치지정 delete를 호출하는 쪽으로 가지 않는다. 위치지정 delete는 오로지 생성자에서 예외가 발생했을 때만 호출된다.

 

그리고 클래스 내에서 operator new, delete를 하나라도 선언했다면 외부에 존재하는 다른 operator new, delete들의 이름이 가려지기 때문에 신경을 써야한다.

 

이름이 가려지는 문제는 new, delete를 구현해둔 기본 클래스를 만들어서 파생 클래스에 상속시키는 방법이 있다.

 

class StandardNewDeleteFormss {
public:
    // 기본형
    static void* operator new(std::size_t size) throw(std::bac_alloc)
    { return ::operator new(size); }
    static void operator delete(void *pMemory) throw()
    { ::operator delete(pMemory); }
    
    // 위치지정
    static void* operator new(std::size_t, void *ptr) throw()
    { return ::operator new(size, ptr); }
    static void operator delete(void *pMemory, void *ptr) throw()
    { ::operator delete(pMemory, ptr); }
    ...
};

class Widget: public StandardNewDeleteForms {
public:
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;
    
    // 또다른 위치지정
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
    static void operator delete(void *pMemory, std::ostream&) throw();
};

 

◾ operator new 함수의 위치지정 버전을 만들 때는, 이 함수와 짝을 이루는 위치지정 버전의 operator delete 함수도 꼭 만들어 주세요. 이 일을 빼먹었다가는, 찾아내기도 힘들여 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험하게 됩니다.

◾ new 및 delete의 위치지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의해 주세요.

'도서 > Effective C++' 카테고리의 다른 글

9. 그 밖의 이야기들  (0) 2022.12.07
7. 템플릿과 일반화 프로그래밍  (0) 2022.12.05
6. 상속, 그리고 객체 지향 설계  (0) 2022.12.04
5. 구현  (0) 2022.12.03
4. 설계 및 선언  (0) 2022.12.01

+ Recent posts