반응형
항목 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 |
댓글