본문 바로가기
프로그래밍언어/C++

[Effective C++] 항목 47~49

by 목가 2018. 4. 10.
반응형
항목 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자.

STL 반복자 5종류
-입력 반복자; 전진만 가능, 한번에 한칸, 읽기 한번만 가능(istream_terator)
-출력 반복자; 전진만 가능, 한번에 한칸, 쓰기 한번만 가능(ostream_terator)
-순방향 반복자; 입력반복자 + 출력반복자(TR1의 해시컨테이너, slist(단일 연결리스트))
-양방향 반복자; 순방향 반복자 + 뒤로 가기 가능(list, set, map 등)
-임의 접근 반복자; 반복자 산술연산 가능( 가장 강력, vector. deque, string 등)

임의 접근 반복자만 산술 연산 가능.ex) 지시자 +=2.
-> 다른애들도 산술연산 가능하도록 템플릿으로 만들어 준다.

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);
-> +1씩 여러번 하도록 구현했다고 가정한 경우
-> 임의 접근 반복자의 경우는 손해.(임의 접근 반복자는 한번에 접근 가능하므로)

수정코드
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if(iter가 임의 접근 반복자)
{
iter += d;
}
else
{
if(d>=0) { while(d--) ++iter;}
else { while (d++) --iter;}
}
}

-임의 접근 반복자 확인 방법
특성정보(traits) 확인; 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체

STL반복자의 특성정보는 다음처럼 구현되어있다.
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
}

여기서 iterator_category 란?
각 타입에 대한 반복자 범주를 알려주는 typedef.

ex) list
template<...>
class list
{
 public:
  class iterator
  {
public:
 typedef bidirectional_iterator_tag iterator_category;
  }
}

위 코드는 사용자 정의 타입에 대해서는 OK, 반복자 실제 타입이 포인터인 경우 동작X(포인터로 typedef 불가능).
-> 부분 템플릿 특수화 버전 제공으로 해결

template<typename IterT>
struct iterator_traits<IterT*>     //부분 템플릿 특수화(기본제공 포인터 타입일 경우 이 template이 호출된다.)
{
 typename random_access_iterator_tag iterator_category;  //포인터 동작원리가 임의접근 반복자와 같으므로
}


따라서 위의 STL 구문을 모방하여 advance 에 적용
if(iter가 임의 접근 반복자
=> if(typeid(typename std::iterator_tratis<IterT>::iterator_category) == typeid(std::random_access_iterator_tag))

그러나 위 template code에서 IterT 타입은 컴파일 단계에서 파악되고, if 구문은(typeid함수) 코드 실행시간에 호출됨.(ex)에러시 시간낭비, 또한 컴파일 문제가 있다(항목48에서 다룸)
-> 모두 컴파일 단계에서 하도록 개선 (오버로딩 사용)

<오버로딩 template code는 p338.>

<실행코드>
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
 doAdvance( iter, d, typename std::iterator_traits<IterT>::iterator_category() );
}

이것만은 잊지 말자!
- 특성정보클래스(iterator_traits 구조체)는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어냅니다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현합니다.
- 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if...else 점검문을 구사할 수 있습니다.


항목 48: 템플릿 메타프로그래밍, 하지 않겠는가?

템플릿 메타프로그래밍(TMP)이란?
 컴파일 도중에 실행되는 템플릿 기반의 프로그래밍

강점
1. 까다롭거나 불가능한 일을 쉽게 할 수 있다.
2. TMP는 컴파일이 진행되는 동안에 실행되기 때문에, 기존 작업을 runtime 영역에서 compile time 영역으로 전환할 수 있다.
(컴파일시 에러 찾기 가능, 실행코드 크기 작아짐, 실행시간 짧아짐, 메모리 절약. 하지만 컴파일 시간 증가)


template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if(typeid(typename std::iterator_tratis<IterT>::iterator_category) == typeid(std::random_access_iterator_tag))
{
<b><font color="#ff0000">iter += d;</font></b>
}
else
{
if(d>=0) { while(d--) ++iter;}
else { while (d++) --iter;}
}
}

->비효율 적인 이유
- typeid 연산자 사용시 타입 점검이 컴파일 도중이 아닌 런타임 중에 일어남.(함수)
- 타입 점검 코드가 실행파일에 들어감.(실행코드 비대화)

-> TMP로 바꾼다(특성정보(traits) 방법). - 항목 47에서 개선된 코드방식(typeid 미사용)이 TMP방식

또한 typpeid 방법은 컴파일 문제를 일으킬 수 있음.

std::list<int>iterator iter;
...
advance(iter, 10);    // iter을 10개 원소만큼 앞으로 옮김.  // 컴파일 에러
-> 위 코드에서 빨간색 부분에서 if문은 타지 않더라도 컴파일 중 iter += d;가 잘못된 코드이기때문에 컴파일 에러 발생.
(list는 양방향 반복자이기 때문에 += 을 지원하지 않는다.)


TMP의 기능
- 변수선언, 루프 실행, 함수 작성 및 호출
ex) TMP에서 if문 구현 원할 시 if문이 아닌 위 방법처럼 템플릿 및 템플릿 특수화 버전을 사용
ex) 루프 : 재귀를 사용해서 루프의 효과를 낸다.(재귀식 템플릿 인스턴스화)

template<unsigned n>
struct Factorial
{
 enum { value = n * Factorial<n-1>::value };     // Loop 생성
};

template<>
struct Factorial<0>
{
 enum{ value = 1 };      // 나열자 둔갑술(enum hack), 컴파일 단계에서 변수로서 작동
}

=> 호출
int main()
{
 std::cout << Factorial<5>::value;
 std::cout << Factorial<10>::value;
}


TMP의 장점 활용 분야 3가지.
- 치수 단위의 정확성 확인; 
- 행렬 연산의 최적화; 표현식 템플릿
- 맞춤식 디자인 패턴 구현의 생성; 생성식 프로그래밍


이것만은 잊지말자!
- 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환하는 효과를 냅니다. 따라서 TMP를 쓰면 선행 에러 탐지와 높은 런타임 효율을 손에 거머쥘 수 있습니다.
- TMP는 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있으며, 또한 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 쓸 수 있습니다.


8장 new와 delete를 내 맘대로
항목 49: new 처리자의 동작 원리를 제대로 이해하자

operator new 요청이 메모리가 부족하여 할당할 수 없을 때 operator new 함수는 예외를 던진다.
ex)옛 컴파일러 환경 - null 반환

그런데, operator new가 예외를 던지기 전에, 이 함수는 사용자 쪽에서 지정할 수 있는 에러 처리 함수를 우선적으로 호출한다.
이 에러 처리 함수를 "new 처리자"(new-handler, 할당에러처리자) 라고 한다.

new 처리자는 STL의 set_new_handler 라는 함수를 이용해서 호출 가능

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

사용예제
void outOfMem()
{
 std::cerr << "Unable to satisfy request for memory\n";
 std::abort();
}

int main()
{
 std::set_new_handler(outOfMem);
 int *pBigDataArray = new int[1000000000000000000000];
 ...
}

그러나 만약 outOfMem() 에서 cerr에 메세지를 쓰는 과정에서 메모리가 또 필요할 경우 또 다른 문제 야기함.
따라서 그런 문제들을 new 처리자에서 잘 처리해 주도록 하는 몇가지 지침은 다음과 같습니다.

- 사용할 수 있는 메모리를 더 많이 확보합니다.
ex) operator new의 메모리 확보가 성공할 수 있도록, 프로그램 시작시 메모리 블록을 크게 할당해 두었다가 new에 사용

- 다른 new 처리자를 설치합니다.
현재의 new 처리자가 더이상 가용 메모리를 확보할 수 없을 경우 자기 몫까지 해줄 다른 new 처리자를 사용하는 방법.
-> new 처리자 함수가 호출되면 다른 new 처리자를 설치한다. new 처리자가 다시 호출되면(메모리 찾기위해 반복호출됨) 새로 설치된 new 설치자가 호출됨.

- new 처리자의 설치를 제거합니다.
set_new_handler에 Null포인터를 넘김. new 처리자가 설치된 것이 없으면 메모리할당 실패했을 때의 예외를 던짐.

- 예외를 던집니다.
 bad_alloc이나 여기서 파생된 타입의 에러를 던진다.

- 복귀하지 않습니다.
 abort나 exit 호출


클래스 타입에 따라서 메모리 할당 실패 처리를 다르게 하고 싶은 경우
-> 해당클래스에서 자체 버전의 set_new_handler 및 operator new를 만든다.

ex) 
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;   //operator new가 할당 실패시 호출될 new 처리자 함수를 가리킴.
}

std::new_handler Widget::currentHandler = 0;  //정적 클래스 맴버 정의는 클래스 바깥,Null로 초기화, 클래스 구현파일에 삽입.

std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
 std::new_handler oldHandler = currentHandler;
 currentHandler = p;  // 새로운 new 처리자로 갱신
 return oldHandler;    //전에 저장해둔 new 처리자 반환
}

// operator new의 구현 시나리오
1. 표준 set_new_handler 함수에 Widget의 new 처리자를 넘겨서 호출한다. (widget에서 만든 new 처리자를 표준으로 사용)

2. 전역 operator new를 호출하여 메모리 할당 수행 -> 할당 실패 시 Widget의 new 처리자를 호출.(위에서 만든 new 처리자를 표준으로 설정했기 때문) -> 마지막까지 전역 operator new의 메모리 할당 시도 실패시 bad_alloc 예외 던짐 -> 전역 new 처리자를 원래의 것으로 돌려 놓고 예외를 던져야 함. ( 이 과정을 NewHandlerHolder 클래스로 자동 관리)

//구현
class NewHandlerHolder           //자원관리 클래스( 객체 생성시 자원획득, 소멸시 해제)
{
 public:
  explicit NewHandlerHolder(std::new_handler nh);
  : handler(nh) { }
 ~NewHandlerHolder()
 { std::set_new_handler(handler); }          //전역 new 처리자를 원래 것으로 복원

private:
 std::new_handler handler;
 NewHandlerHolder(const NewHandlerHolder&);
}

//위 클래스를 이용하여 operator new를 간단히 구현
void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
 NewHandlerHolder h(std::set_new_handler(currentHandler));   //생성자 호출
 return ::operator new(size);              //메모리 할당 수행, 실패시 예외 던짐
}                                            //소멸자 호출(이전의 new 처리자가 자동 복원)


//호출부

void outOfmem();            //메모리할당 실패시 호출

Widget::set_new_handler(outOfMem);     //new 처리자 함수로 설치

Widget *pw1 = new Widget;       //실패시 outOfMem 호출

std::sstring *ps = new std::string;    // 할당 실패시 전역 new 처리자 함수가(있으면) 호출

Widget::set_new_handler(0);      // Null로 설정

Widget *pw2 = new Widget;      //실패시 예외를 바로 던짐(new 처리자 없음).



자원 관리 객체를 통한 할당에러 처리를 구현하는 코드는 어떤 클래스를 써도 같을 것 -> 템플릿화

// 위의 구현을 템플릿화(mixin : 파생클래스들이 한 가지의 특정 기능만 물려받도록 설계)
-> 파생 클래스마다 클래스 데이터(원래의 new 처리자를 기억해 두는 정적 멤버 데이터(currentHandler))의 사본이 따로 존재하게 된다.

template<typename T>
class NewHandlerSupport          //mixin 양식의 기본클래스
{
 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;
};

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);
}

//클래스별로 만들어지는 currentHandler 멤버를 널로 초기화
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

//Widget 클래스에 set_new_handler 기능 추가 (template을 상속받으면 끝)
class Widget: public NewHandlerSupport<Widget>
{ ... };


위 코드에서 typename T는 사용되지 않는다. 기본클래스 객체의 서로 다른 사본만 필요할 뿐이다.
조금 이상해 보일 수 있는 만큼 패턴 이름도 "신기하게 반복되는 템플릿 패턴"

- 예외불가(nothrow) 형태 : 메모리 할당 실패시 널을 반환하도록 함.

class Widget {..};
Widget *pw1 = new Widget;     //bad_alloc 예외 던짐
if(pw1 == 0) ...        //절대 0일 수 없다

widget *pw2 = new (std::nothrow) Widget;    //할당 실패시 null 반환

if(pw2 == 0) ...      //if문이 true 가능

-> 그렇지만 nothrow를 사용해도 생성자 안에서 호출되는 또 다른 생성자(new 할당)의 예외처리까지 null로 할 수 없음.

이것만은 잊지 말자!
- set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있습니다.
- 예외불가(nothrow) new는 영향력이 제한되어 있습니다. 메모리 할당 자체에만 적용되기 때문입니다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있습니다.


반응형

'프로그래밍언어 > C++' 카테고리의 다른 글

[Effective C++] 항목 44~46  (0) 2018.04.10
[Effective C++] 항목 41~43  (0) 2018.04.10
[Effective C++] 항목 38~40  (0) 2018.04.10
[Effective C++] 항목 35~37  (0) 2018.04.10
[Effective C++] 항목 29~31  (0) 2018.04.10

댓글