12. 항목11: 소멸자에서는 예외가 탈출못하게
• 클래스 소멸자가 호출되는 경우는?
• 객체가 통상적인 조건에서 소멸되는 것인데, 통상적인 조건이란
지역변수로 선언된 객체가 유효범위(scope)를 벗어났을때와 객
체가 직접 삭제(delete)될 때
• 예외 처리 매커니즘에 의해 객체가 소멸되는 것인데, 예외 전파
(exception propagation) 과정의 일부분으로 스택 되김가기 진
행될 때
• 즉, 소멸자가 호출되었을 때 예외일수도 아닐 수도 있다.
13. C++ terminate함수
• 프로그램의 실행을 끝장낸다.
• 지역 객체조차도 소멸되지 않는다.
• 어떤 예외에 대한 처리가 진행되고 있는 동안 또 다른 예외 때
문에 프로그램 흐름이 소멸자 함수를 떠나면, C++ terminate 함
수를 호출하게 된다.
• 이를 고려하여 어떤 상황에서는 소멸자를 작성할 때 예외가 발
생된 상태에 있다고 가정하여야 한다.
26. catch() 문에서 허용되는 타입변환
• 타입이 있는 포인터로부터 타입이 없는 포인터로 바꾸는 경우
• 즉, const void* 포인터를 받는 catch 문은 어떤 포인터 타입의
예외이든지 잡아 낼 수 있다.
• catch (const void*) …
27. 매개변수 전달과 예외 전파 사이 차이점3
• catch 문은 등장한 순서에 따라 사용된다.
• 파생 클래스 타입을 받는 catch 문이 준비되어 있다고 해도 순
서가 제대로 되어 있지 않으면, 기본 클래스 타입을 받는 catch
문이 파생 클래스 타입의 예외를 잡아낼 수 있다.
31. 포인터에 의한 예외전달
• 예외 객체가 힙에 할당된 주소라면 메모리 누수를 막기 위해 포
인터를 삭제해야 한다.
• 그런데 예외 객체가 힙에 할당되지 않는다면?
• 사용자에 따라 다르다.
• 어떤 사용자는 전역 객체나 정적 객체의 주소를 넘길 수 있고,
어떤 사용자는 힙에 할당된 예외 객체의 주소를 넘길 수 있다.
• C++ 기본 제공 표준 예외는 객체에 대한 포인터가 아니라 모두
객체이다. 포인터를 사용하면 이 예외들을 사용할 수 없다.
32. 값에 의한 예외받기(catch-by-value)
• 예외 삭제 고민에서 해방
• C++ 표준 예외와도 잘 맞다.
• 전달되는 예외 객체는 늘 두 번씩 복사되어야 한다.
• 슬라이스 문제(slicing problem): 발생 시에는 파생 클래스의 객
체였다가, 기본 클래스를 받는 catch 문에 들어가면 파생 클래
스 부분에 추가되었던 데이터가 싹둑 잘려 나가는 현상
41. 예외 지정의 고려할 점
• 어떤 예외가 발생될지를 알려준다는 면에서 문서화에 도움이
된다.
• C++는 예외 지정의 위배가 발생하면 기본적으로 프로그램을
일 시 중단시킨다.
• 컴파일러는 예외 지정에 대해 부분적인 일치성 점검만 수행하
고, 프로그래머는 예외 지정의 일치성을 어기기 쉽다.
42. 항목15: 예외처리 비용을 제대로 알자
• 예외 처리 기능을 전혀 쓰지 않았을 때에 비용은
• 어떤 객체가 생성 과정을 완료했는지 체크하는 데에 내부적으
로 사용되는 자료구조에 대한 메모리가 소모된다.
• 이 자료구조를 업데이트하는 데에 필요한 시간이 소모된다.
• 결론적으로 예외 처리 기능을 배제하고 컴파일한 프로그램은
예외 처리를 지원하도록 컴파일한 것에 비해 속도도 빠르고 크
기도 작다.
43. try 블록으로 생기는 비용
• try 블록이 소스에 들어가기만 하면, 즉 예외를 처리하겠다고 작
정하면 무조건 지불해야할 비용이다.
• 예외 지정 기능에 대해서도 try 블록과 비슷한 양의 코드가 생성
되기 때문에, 예외 지정에 들어가는 try 블록과 비슷하다.
• 예외가 발생되는 경우는 드물기 때문에 예외 발생 시 소모되는
비용은 큰 관심사가 되지 못한다.
• 우선 가능하다면 예외 기능을 지원하지 않도록 컴파일한다.
44. 항목16: 80-20 법칙
• 프로그램 리소스의 80%는 전체 실행 코드의 약 20%만이 사용
한다.
• 실행시간의 80%는 실행 코드의 약 20%만 소모한다.
• 메모리의 80%는 실행 코드의 약 20%만이 사용한다.
• 디스크 접근 회수의 80%는 실행코드의 20%가 접근한 회수다.
• 프로그램 유지보수에 들어가는 수고의 80%는 실행 코드의 20%
에 집중된다.
• 아무 곳이나 골라잡고 효율을 향상시키려고 애쓰지 마라.
45. 프로그램 프로파일러
• program profiler
• 성능 향상을 만들어 낼 수 있는 20%를 판별하기 위해 사용한다.
• 사용자가 관심을 두고 있는 리소스를 직접 측정해주는 도구가
필요하다.
• 수행 성능 문제에 대처하는 최선의 길은 가능한 많은 데이터를
사용해서 소프트웨어를 프로파일링하는 것이다.
46. 항목17: 지연 평가(lazy evaluation)
• 지연(to be lazy): 어떤 일을 하긴 하되 그 일을 하는 코드의 실
행을 피하는 방법
• 지연 평가: 지연 평가를 사용해서 만든 C++ 클래스는 어떤 처
리 결과가 진짜로 필요해질 때가지 그 처리를 미룬다. 어떤 컴퓨
팅 작업을 수행하는 데에 있어서 그 작업 결과가 진짜로 요구되
기 전에는 그것을 하지 않는다.
53. 수치 계산에 있어서 지연 평가
• 두 값 사이의 의존 관계를 저장해야 한다.
• 값과 의존 관계를 저장하는 자료구조도 필요하다.
• 대입, 복사, 덧셈 같은 연산자에 대한 오버로딩도 필요하다.
• 이런 수고로움이 따르지만 상당량의 수행 시간/가용 메모리 절
약을 해준다는 장점이 있다.
54. 항목18: 예상되는 계산 결과를 미리 준비
• 과도 선행 평가(over-eager evaluation)
• 현재 요구된 것 이외에 더 많은 작업을 미리 해둠으로써 소프트
웨어의 성능을 향상시킨다.
56. 이미 계산이 끝났고 다시 사용될 것 같은 값을 캐싱하는 방법
• 데이터베이스에 대한 질의 반복하면 데이터베이스에 부하가 생
긴다.
• 이를 방지하기 위해 이전에 뽑아 낸 데이터를 캐싱해두는 함수
를 만든다.
• 이미 탐색한 데이터는 데이터베이스가 아닌 캐시를 통해 가져
오도록 만든다.
58. 미리가져오기(prefetching)
• 디스크 컨토롤러는 디스크에서 데이터를 읽을 때, 프로그램 쪽
에서 아주 적은 양만 요구했음에도 불구하고 블록 하나 혹은 섹
터 하나를 왕창 읽는다.
• 조금씩 여러 번 읽는 것보다 한 번에 많이 읽는 쪽이 더 빠르다.
• locality of reference
62. 공간과 시간은 함께 절약하기 힘들다.
• 계산 결과를 캐싱하려면 메모리 사용량이 필연적으로 높아진다.
• 계산값을 재생성하는데에는 시간이 들지 않는다.
• prefetching 방법을 쓰려면 미리 가져온 명령어나 데이터를 저
장해 둘 공간이 필요하다.
• 하지만 저장해 놓은 데이터나 명령어를 접근하는 데에 필요한
시간은 줄어든다.
• 결국 메모리를 많이 쓰면 속도가 빨라진다.
66. 임시객체 생성을 방지하는 방법
• 임시 객체가 함수 호출 성사를 위해 생성되었다가 소멸되는 일
은 편리하긴 하지만 불필요한 낭비이다.
• 이를 막는 일반적인 방법은 두 가지이다.
• 코드를 다시 설계해서 이런 변환이 일어나지 않게 하는 것
• 타입변환이 불필요하도록 소프트웨어를 수정하는 것
67. 암시적 타입변환이 이루어지는 때
• 객체가 값으로 전달될 때 혹은 상수 객체 참조자(reference-to-
const)타입의 매개변수로 객체가 전달될 때
• 비상수 객체 참조자(reference-to-non-const) 타입의 매개변수
로 객체가 전달될 때에는 암시적 타입변환이 일어나지 않는다.
77. 단독 형태 연산자와 대입 형태 연산자
• 일반적으로 대입 형태 연산자는 단독 형태 연산자보다 효율적
이다. 단독 형태 연산자는 새 객체를 반환해야 하기 때문에, 임
시 객체를 생성하고 소멸하는 비용이 소모되지만, 대입 형태 연
산자는 왼쪽인자에다가 처리결과를 기록하기 때문에, 이 연산자
의 반환값을 담을 임시 객체를 만들어 놓을 필요가 없다.
78. 단독 형태 연산자와 대입 형태 연산자
• 대입 형태 연산자와 단독 형태 연산자를 동시에 제공함으로써
클래스 사용자에게 효율과 편리성 사이에서 선택할 수 있는 기
회를 준다.
81. 항목23: 정 안되면 다른 라이브러리 사용
• 이상적인 라이브러리는 작고, 빠르고, 강력하고, 유연하고, 확장
도 가능하고, 직관적이고, 어디든 쓸 수 있고, 플랫폼 지원도 좋
아야 하고, 사용상의 제약에 대해 자유롭고, 버그도 없어야 한다.
• 하지만 이는 이상일 뿐이다.
• 속도, 범용성, 견고성 등 중에서 무엇을 우선순위로 할 것인지
선택해서 라이브러리를 구현한다.
• 소프트웨어에서 사용하는 라이브러리만 교체해도 성능 향상을
이뤄낼 수 있다.
82. 항목24: 가상함수, 다중 상속 등등 비용 파악
• virtual function
• virtual table(vtbl): 보통 함수 포인터의 배열(어떤 컴파일러는 배
열 대신에 linked list를 사용하기도 한다). 이 테이블은 가상 함
수를 선언했거나 상속받은 클래스에 무조건 생기고, vtbl의 각
요소는 해당 클래스에서 정의한 가상 함수 코드의 시작주소이
다.
85. 가상함수에 들어가는 비용
• 가상 테이블을 담는 메모리가 필요하다.
• 어떤 클래스에 대해 만들어지는 vtbl의 크기는 그 클래스에 선
언된 가상함수(기본 클래스에서 상속받는 것까지 합해서)의 수
에 비례한다.
• 한 클래스의 vtbl은 프로그램 이미지 안에 딱 하나만 있어야 하
는데, 컴파일러는 vtbl을 어디에 둘 것인가?
86. 가상 테이블 포인터
• vptr
• 가상 함수를 선언한 클래스로부터 만들어진 객체에는 그 클래
스의 가상 함수를 가리키는 데이터 멤버가 하나 숨겨져 있다.
• vptr는 놓이는 객체 내의 위치는 컴파일러만 알고 있다.
• vptr은 가상 함수에 들어가는 두 번째 비용이다.
• 객체가 별로 크지 않은 경우 vptr의 비용은 만만치 않다.
87. 수행 성능을 저하하는 가상 함수 비용
• 인라인(inline): 컴파일 도중에, 호출 위치에 호출되는 함수의 몸
체를 끼어 넣는다.
• 가상(virtual): 호출할 함수를 런타임까지 기다려 결정한다.
• 가상 함수는 함수의 인라인 효과를 포기해야 한다.
91. 런티임 타입 식별(RTTI)
• runtime type identification
• 실행 중에 객체와 클래스의 정보를 알아낼 수 있게 하는 기능
• 그 정보를 저장해 둘 공간이 필요하다.
• C++에 의하면 객체의 동적 타입을 정확히 뽑아낼 수 있으려면
그 타입에 가상 함수가 최소한 하나 있어야 한다.
• 이는 가상 함수 테이블과 유사하다.