1. C++에 왔으면 C++의 법을 따릅시다.
[항목 1] C++를 언어들의 연합체로 바라보는 안목은 필수
* 다중 패러다임 프로그래밍 언어
= 절차적 프로그래밍 + 객체 지향 + 함수식 + 일반화 + 메타프로그래밍 + etc...
= C + 객체 지향 + 템플릿 + STL
cf. C++를 사용한 효과적인 프로그래밍 규칙은 경우에 따라 달라집니다. 그 경우란, 바로 C++의 어떤 부분을 사용하느냐 입니다.
[항목 2] #define을 쓰려거든 const, enum, inline
cf. 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각합시다.
cf. 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각합시다.
[항목 3] 낌새만 보이면 const를 들이대 보자
* 상수 멤버 함수
> 비트수준 상수성 : 물리적 상수성
> 논리적 상수성 : 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되(mutable), 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있음.
* 상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
> 비상수 멤버 함수(){ 상수멤버 함수(); }
cf. const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.
cf. 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 논리적인(개념적인) 상수성을 사용해서 프로그래밍을 해야 합니다.
cf. 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요.
[항목 4] 객체를 사용하기 전에 반드시 그 객체를 초기화하자
* 초기화 순서
> 기본 클래스는 파생 클래스보다 먼저 초기화
> 클래스 데이터 멤버는 선언 순서대로 초기화
* 비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해짐
= 별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 정해져 있지 않다.
= 서로 다른 번역 단위에 정의된 비지역 정적 객체들 사이의 상대적인 초기화 순서는 정해져 있지 않다.
> 정적 객체 : 생성된 시점부터 프로그램이 끝날 때까지 살아있는 객체
> 번역 단위 : 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드
-> 비지역 정적 객체를 지역 정적 객체(싱글톤)로 바꿈
(다중 스레드 문제(경쟁 상태 race condition) 가능 -> 참조자 반환 함수 이용)
cf. 기본제공 타입의 객체는 직접 손으로 초기화합니다. 경우에 따라 저절로 되기도 하고 안되기도 하기 때문입니다.
cf. 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용합시다. 그리고 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언도니 순서와 똑같이 나열 합시다.
cf. 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 합니다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 됩니다.
2. 생성자, 소멸자 및 대입 연산자
[항목 5] C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자
* 기본 생성자, 기본 복사 생성자, 기본 대입 연산자, 기본 소멸자
cf. 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있습니다.
(기본 클래스에 private로 기본 함수를 만들면 파생에서 암시적으로 기본 함수를 만들지 못함)
[항목 6] 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
cf. 컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private로 선언한 후 구현(정의)은 하지 않은 채로 두십시오. Uncopyable과 비슷한 기본 클래스를 쓰는 것도 한 방법 입니다.
[항목 7] 다형성을 가진 기본 클래스에서 소멸자를 반드시 가상 소멸자로 선언하자
* 팩토리 함수 : 새로 생성된 파생 클래스에 대한 기본 클래스 포인터를 반환하는 함수
* 가상 함수를 C++에 구현 하려면 클래스에 별도의 자료구조가 하나 들어감. (객체 아님)
* vptr : 가상함수테이블 포인터
(LLP64 데이터 모델을 채택한 64비트의 경우 포인터 크기가 64비트)
* vtbl : 가상 함수 테이블, 가상 함수 테이블 포인터의 배열
* 순수 가상 소멸자를 두면 편함 (추상 클래스 : 순수 가상 함수를 갖는 클래스)
cf. 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 합니다.
cf. 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 합니다.
[항목 8] 예외가 소멸자를 떠나지 못하도록 붙들어 놓자
cf. 소멸자에서는 예외가 빠져나가면 안 됩니다. 마냥 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 합니다.
cf. 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.
[항목 9] 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자
* 기본 클래스의 생성자가 호출될 동안에는, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다.
[항목 10] 대입 연산자는 *this의 참조자를 반환하게 하자
cf. 대입 연산자는 *this의 참조자를 반환하도록 만드세요.
[항목 11] operator= 에서는 자기 대입에 대한 처리가 빠지지 않도록 하자
* 중복 참조(aliasing) : 여러 곳에서 하나의 객체를 참조, 이 때문에 자기 대입이 생김
* 자기 대입인지 검사, 자기 대입 시 원본을 삭제
cf. operator=을 구현할 때, 어던 객체가 그 자신에 대입되는 경우 제대로 처리 하도록 만듭시다. 원본 객체와 복사 대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞밖구기 기법을 써도 됩니다.
[항목 12] 객체의 모든 부분을 빠짐없이 복사하자
cf. 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 합니다.
cf. 클래스의 복사 함수 두 개를 구현할 때, 한 쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 마세요. 그 대신, 공통된 동작을 제 3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결 합니다.
3. 자원 관리
[항목 13] 자원 관리에는 객체가 그만!
* 자원 관리에 객체를 사용하는 방법의 특징
> 자원을 획득한 후에 자원 관리 객체에게 넘깁니다.
> 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 합니다.
* RAII : Resource Acquisition Is Initialization (자원 획득 즉 초기화)
* RCSP : reference-counting smart pointer (참조 카운팅 방식 스마트 포인터)
cf. 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용합시다.
cf. 일반적으로 널리 쓰이는 RAII 클래스는 tr1::shared_ptr 그리고 auto_ptr입니다. 이 둘 가운데 tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋습니다. 반면, auto_ptr은 복사되는 객체(원본 객체)를 null로 만들어 버립니다.
[항목 14] 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
* 객체 복사할 때...
> 복사를 금지합니다. (Uncopyable 상속)
> 관리하고 있는 자원에 대해 참조 카운팅을 수행합니다. (tr1::shared_ptr)
> 관리하고 있는 자원을 진짜로 복사합니다. (깊은 복사(deep copy))
> 관리하고 있는 자원의 소유권을 옮깁니다. (auto_ptr)
cf. RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사 하느냐에 따라 RAII 객체의 복사 동작이 결정됩니다.
cf. RAII 클래스에 구현하는 이반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리 하는 것 입니다. 하지만 이 외의 방법들도 가능하니 참고해 둡시다.
[항목 15] 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자
cf. 실제 원을 직접 접근 해야 하는 기존 API들도 많기 때문에, RAII 크래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을 열어 주어야 합니다.
cf. 자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능합니다. 안전성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면 암시적 변환이 괜찮습니다.
[항목 16] new 및 delete를 사용할 때는 형태를 반드시 맞추자
* 단일 객체의 메모리 배치구조(layout)는 객체 배열에 대한 메모리 배치구조와 다르다.
* 배열을 위해 만들어지는 힙 메모리에는 대개 배열 원소의 개수가 박혀 들어간다.
* 그렇기에 delete 연산자는 소멸자가 몇 번 호출될 지 쉽게 알 수 있다.
cf. new 표현식에 []를 썼으면, 대응되는 delete 표현식에도 []를 써야 합니다. 마찬가지로 new 표현식에 []를 안 썼으면, 대응되는 delete 표현식에도 []를 쓰지 말아야 합니다.
[항목 17] new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자
cf. new로 생성한 객체를 스마트 포인터롤 넣는 코드는 별도의 한 문장으로 만듭시다. 이것이 안 되어 있으면, 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래 될 수 있습니다.
4. 설계 및 선언
[항목 18] 인터페이스 설게는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
* explicit 키워드를 이용하여 암묵적 변환 방지
* 교차 DLL 문제(cross-DLL problem) : 객체 생성 시에 어떤 동적 링크 라이브러리(dynamically linked library:DLL)의 new를 썼는데 그 객체를 삭제 할 때는 이전의 DLL과 다른 DLL에 있는 delete를 썼을 경우, new/delete 짝이 싱행되는 DLL이 달라서 꼬이게 되어 런타임 에러가 발생
cf. 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵습니다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록 고민하고 또 고민합시다.
cf. 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본 제공 타입과의 동작 호환성 유지하기가 있습니다.
cf. 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있습니다.
cf. tr1::shared_ptr은 사용자 정의 삭제자를 지원 합니다. 이 특징 때문에 tr1::shared_ptr은 교차 DLL 문제를 막아 주며, 뮤텍스 등을 자동으로 잠금 해제하는 데 쓸 수 있습니다.
[항목 19] 클래스 설계는 타입 설계와 똑같이 취급하자
cf. 클래스 설계는 타입 설계 입니다. 새로운 타입을 정의 하기 전에, 이번 항목에 나온 모든 고려 사항을 빠짐없이 점검해 보십시오.
[항목 20] '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다
* 복사 손실 문제(slicing problem) : 파생 클래스 객체가 기본 클래스 객체로서 전달 되는 경우 이 객체가 값으로 전달된면 기본 클래스의 복사 생성자가 호출되고, 파생 클래스 객체로 동작하게 해 주는 특징들이 '싹둑 잘려' 떨어짐
* 참조자는 보통 포인터를 써서 구현된다. 즉, 참조자를 전달한다는 것은 결국 포인터를 전달한다는 것과 일맥상통한다.
* 따라서 전달하는 객체의 타입이 기본제공 타입(int 등)일 경우에는 참조자로 넘기는 것보다 값으로 넘기는 편이 더 효율적일 때가 많다.
* 반복자와 함수 객체를 구현할 때는 반드시
1) 복사 효율을 높일 것
2) 복사 손실 문제에 노출되지 않도록 만들 것
* 사용자 정의 타입은 참조자에 의한 전달이 더 적합, 기본 정의 타입과 다르게 취급(바로 레지스터에 등록이라던가) 당할 수 있으며 후에 크기가 커질 수 있기 때문
cf. '값에 의한 전달'보다는 '상수 객체 참조자에 의한 전달'을 선호 합시다. 대체적으로 효율적일 뿐만 아니라 복사 손실 문제까지 막아 줍니다.
cf. 이번 항복에서 다룬 법칙은 기본 제공 타입 및 STL 반복자, 그리고 함수 객체 타입에는 맞지 않습니다. 이들에 대해서는 '값에 의한 전달' 더 적절합니다.
[항목 21] 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자
* 참조자를 반환할 것인가 아니면 객체를 반환할 것인가를 결정 할 때, 어떤 선택을 하든 올바른 동작이 이루어지도록 만들어야 한다. 선택한 결과를 최대한 저비용으로 만드려면 어떻게 해야 하는지 파악하느라 끙끙대는 일은 컴파일러 제작사에게 맡긴다.
cf. 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대로 하지 마세요.
[항목 22] 함데이터 멤버가 선언될 곳은 private 영역임을 명심하자
* private 데이터 멤버이어야 하는 이유
> 문법적 일관성, 캡슐화
cf. 데이터 멤버는 private 멤버로 선언합시다. 이를 통해 클래스 제작자는 문법적으로 일관성 있느 ㄴ데이터 접근 통로를 제공할 수 있고, 필요에 따라서는 세밀한 접근 제어도 가능하며, 클래스의 불변속성을 강화 할 수 있을 뿐만 아니라, 내부 구현의 융통성도 발휘 할 수 있습니다.
cf. protected는 public보다 더 많이 '보호'받고 있는 것이 절대로 아닙니다.
[항목 23] 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자
* 네임스페이스를 이용하면 더 자연스러움
cf. 멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록 합시다. 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인 확장성도 늘어납니다.
[항목 24] 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자
cf. 어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함해서)에 대해 타입 변환을 해 줄 필요가 있다면, 그 함수는 비멤버이어야 합니다.
[항목 25] 예외를 던지지 않는 swap에 대한 지원도 생각해 보자
* pimpl (point to implementation) : 실제 데이터가 가진 객체를 포인터로 갖고 있음
* 완전 템플릿의 특수화(total template specializatioin)
* 인자 기반 탐색(argument-dependent lookup) 혹은 쾨니그 탐색(Koenig lookup) : 어떤 함수에 어떤 타입의 인자가 있으면, 그 함수의 이름을 찾기 위해 해당 타입의 인자가 위치한 네임스페이스 내부의 이름을 탐색해 들어간다는 규칙
*
> 표준에서 제공하는 swap이 여러분의 클래스 및 클래스 템플릿에 대해 납득할 만한 효율을 보이면, 그냥 아무것도 하지 말고 지내세요
> 표준 swap의 효율이 기대한 만큼 충분하지 않다면(사용자 클래스 혹은 클래스 템플릿이 pimpl 관용구와 비슷하게 만들어져 있을 경우가 대부분...)
1) 사용자 타입으로 만들어진 두 객체의 값을 빛나게 빨리 맞바꾸는 함수를 swap이라는 이름으로 만들고, 이것을 public 멤버 함수로 두십시오. 단, 이 함수는 예외를 던져선 안됩니다.
2) 사용자 클래스 혹은 템플릿이 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap을 만들어 넣습니다. 그리고 1번에서 만든 swap 멤버 함수를 이 비멤버 함수가 호출 하도록 만듭니다.
3) 새로운 클래스(클래스 템플릿이 아니라)를 만들고 있다면, 그 클래스에 대한 std::swap의 특수화 버전을 준비해 둡니다. 그리고 이 특수화 버전에서도 swap 멤버 함수를 호출하도록 만듭니다.
> 사용자 입장에서 swap을 호출 할 때, swap을 호출하는 함수가 std::swap을 볼 수 있도록 using 선언을 반드시 포함 시킵니다. 그 다음에 swap을 호출 하되, 네임스페이스 한정자를 붙이지 않도록 하십시오.
cf. std::swap이 여러분의 타입에 대해 느리게 동작 할 여지가 있다면 swap 멤버 함수를 제공합시다. 이 멤버 swap은 예외를 던지지 않도록 만듭시다.
cf. 멤버 swap을 제공했으면, 이 멤버를 호출하는 비멤버 swap도 제공합니다. 클래스(템플릿이 아닌)에 대해서는, std::swap도 특수화해 둡시다.
cf. 사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출합니다.
cf. 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능합니다. 그러나 std에 어떤 것이라도 새로 '추가'하려고 들지는 마십시오.
5. 구현
[항목 26] 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자
* 생성자를 호출하는 비용과 소멸자를 호출 하는 비용을 줄이기 위하여 늦춤
* 반복문을 사용할 경우 반복문 밖에서 정의하고 사용
cf. 변수 정의는 늦출 수 있을 때까지 늦춥시다. 프로그램이 더 깔끔해지며 효율도 좋아집니다.
[항목 27] 캐스팅은 절약, 또 절약! 잊지 말자
* "어떤 일이 있어도 타입 에러가 생기지 않도록 보장한다." - C++의 동작 규칙 철학
* 캐스팅 문법
- 구형 스타일의 캐스트
> (T) 표현식 - C스타일 캐스트
> T (표현식) - 함수 방식 캐스트
- 신형 스타일의 캐스트, C++ 스타일의 캐스트
> const_cast<T>(표현식) : 객체의 상수성(constness)이나 휘발성(volatileness)를 제거
> dynamic_cast<T>(표현식) : 안전한 다운 캐스팅(safe downcasting)을 할 때 사용
> reinterpret_cast<T>(표현식) : 포인터를 int로 바꾸는 등의 하부 수준 캐스팅
> static_cast<T>(표현식) : 암시적 변환을 강제로 진행할 때 사용
* 코어 덤프 : 유닉스OS 등에서 프로그램이 잘못된 연사능로 인해 죽을 때 그 시점의 정보(대개 메모리의 내용)를 기록한 파일을 생성하는 동작
* 포인터의 변위(offset)를 파생 클래스 포인터에 적용하여 실제의 기본 클래스 포인터 값을 구하는 동작이 런타임에 이루어짐. -> 같은 객체 더라도 캐스팅에 따라 포인터가 달라질 수 있음
* 파생 클래스 오버라이드 멤버 함수에서 기본 클래스 멤버 함수 사용시에 this포인터로 형변환 하기 보다는 바로 기본 클래스 멤버 함수를 호출
* dynamic_cast : 어떤 구현 환경의 경우 클래스 이름에 대한 문자열 비교 연산에 기반을 둠 -> 성능이 느림
> 피하는 방법 : 파생 클래스 객체에 대한 포인터를 컨테이너에 담아 사용, 가상 함수 제공
> 폭포식(cascading) dynamic_cast는 피한다.
cf. 다른 방법이 가능하다면 캐스팅은 피하십시오. 특히 수행 성능에 민감한 코드에서 dynamic_cast는 몇 번이고 다시 생각하십시오. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해 보십시오.
cf. 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 해 보십시오. 이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출 할 수 있게 됩니다.
cf. 구형 스타일 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호 하십시오. 발견하기도 쉽고, 설계자가 어떤 역할을 의도 했는지가 더 자세하게 드러납니다.
[항목 28] 내부에서 사용하는 객체에 대한 "핸들"을 반환하는 코드는 되도록 피하자
* 클래스 데이터 멤버는 아무리 숨겨도 그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다.
* 어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면, 이 함수의 호출부에서 그 데이터의 수정이 가능하다.
* 핸들(handle) : 다른 객체에 손을 댈 수 있게하는 매개자
* 무효참조 핸들(dangling handle) : 핸들이 있기는 하지만 그 핸들을 따라갔을 때 실제 객체의 데이터가 없음
cf. 어떤 객체의 내부 요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것은 되도록 피하세요. 캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작을 할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화 할 수 있습니다.
[항목 29] 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자
* 예외 안정성을 가진 함수가 예외가 발생시 다음과 같이 동작
> 자원이 새도록 만들지 않습니다.
> 자료구조가 더럽혀지는 것을 허용하지 않습니다.
* 예외 안정성을 갖춘 함수는 다음 보장 중 하나를 제공
> 기본적인 보장(basic guarantee) : 동작 중 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것을 유효한 상태로 보장
> 강력한 보장(strong guarantee) : 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장
> 예외불가 보장(nothrow guarantee) : 예외를 절대로 던지지 않겠다는 보장
* 복사-후-맞바꾸기(copy-and-swap) : 어떤 객체를 수정하고 싶으면 그 객체의 사본을 하나 만들어 놓고 그 사본을 수정하는 것
cf. 예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않습니다. 이런 함수들이 제공할 수 있는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있습니다.
cf. 강력한 예외 안전성 보장은 '복사-후-맞바꾸기' 방법을 써서 구현 할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아닙니다.
cf. 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않습니다.
[항목 30] 인라인 함수는 미주알 고주알 따져서 이해해 두자
* 컴파일러 최적화는 함수 호출이 없는 코드가 연속적으로 이어지는 구간에 적용되도록 설계 되었기 때문에, 인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별(context-specific) 최적화를 걸기가 용이해짐. 실제로 대부분의 컴파일러는 '아웃라인(outline)' 함수 호출에 대해 이런 최적화를 적용시키지 않음
cf. 함수 인라인은 작고, 자주 호출되는 함수에 대해서만 하는 것으로 묶어둡시다. 이렇게 하면 디버깅 및 라이브러리의 바이너리 업그레이드가 용이해지고, 자칫 생길 수 있는 코드 부풀림 현상이 최소화되며, 프로그램의 속력이 더 빨라질 수 있는 여지가 최고로 많아 집니다.
cf. 함수 템플릿이 대개 헤더 파일에 들어간다는 일반적인 부분만 생각해서 이들을 inline으로 선언하면 안됩니다.
[항목 31] 파일 사이의 컴파일 의존성을 최대로 줄이자
* 인터페이스와 구현을 둘로 : 정의부에 대한 의존성(dependencies on definitions)을 선언부에 대한 의존성(dependencies on declarations)으로 바꿈
* 객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않습니다.
* 할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만듭니다.
* 선언부와 정의부에 대해 별도의 헤더 파일을 제공합니다.
* pimple 관용구를 사용하는 클래스 : 핸들 클래스(handle class)
* 특수 형태의 추상 기본 클래스 : 인터페이스 클래스(Interface class)
* 파생 클래스의 생성자 역할을 대신하는 어떤 함수를 만들어 놓고 호출 : 팩토리 함수 혹은 가상 생성자(virtual constructor)
* 핸들 클래스 : 구현부 객체의 데이터까지 가기 위해 포인터를 타야함, 객체 하나씩 을 저장하는데 필요한 메모리 크기에 구현부 포인터의 크기가 더해짐, 구현부 포인터가 동적 할당된 구현부 객체를 가리키도록 어디선가 그 구현부 포인터의 초기화가 일어나야 함.
* 인터페이스 클래스 : 호출되는 함수가 전부 가상함수(함수 호출이 일어날 때 마다 가상 테이블 점프에 따르는 비용이 소모)
* 핸들 클래스와 인터페이스 클래스 모두 inline 함수의 도움을 제대로 끌어내기 힘듦
cf. 컴파일 의존성을 최소화하는 작업의 배경이되는 가장 기본적인 아이디어는 '정의' 대신에 '선언'에 의존하게 만들자는 것 입니다. 이 아이디어에 기반한 두 가지 접근 방법은 핸들 클래스와 인터페이스 클래스입니다.
cf. 라이브러리 헤더는 그 자체로 모든 것을 갖추어야 하며 선언부만 갖고 있는 형태여야 합니다. 이 규칙은 템플릿이 쓰이거나 쓰이지 않거나 동일하게 적용합시다.
6. 상속, 그리고 객체 지향 설계
[항목 32] public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따르도록 만들자
* public 상속은 기본 클래스 객체가 가진 모든 것들이 파생 클래스 객체에도 그대로 적용된다고 단정
cf. public 상속의 의미는 "is-a(...는 ...의 일종)"입니다. 기본 클래스에 적용되는 모든 것들이 파생 클래스에 그대로 적용되어야 합니다. 왜냐하면 모든 파생 클래스 객체는 기본 클래스 객체의 일종이기 때문입니다.
[항목 33] 상속된 이름을 숨기는 일을 피하자
cf. 파생 클래스의 이름은 기본 클래스의 이름을 가립니다. public 상속에서는 이런 이름 가림 현상은 바람직하지 않습니다.
cf. 가려진 이름을 다시 볼 수 있게 하는 방법으로 using 선언 혹은 전달 함수를 쓸 수 있습니다.
[항목 34] 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자
* 멤버 함수 인터페이스는 항상 상속되게 되어 있기 때문입니다. public 상속의 의미는 is-a이므로 기본 클래스에 해당하는 것들은 모두 파생 클래스에도 해당되어야 합니다. 따라서 어떤 클래스에서 동작하는 함수 그 클래스의 파생 클래스에서도 동작해야 맞습니다.
* 순수 가상 함수를 선언하는 목적은 파생 클래스에게 함수의 인터페이스만을 물려주는 것 입니다.
* 단순 가상 함수를 선언하는 목적은 파생 클래스로 하여금 함수의 인터페이스 뿐만 아니라 그 함수의 기본 구현도 물려받게 하자는 것 입니다.
* 비가상 함수를 선언하는 목적은 파생 클래스가 함수 인터페이스와 더불어 그 함수의 필수적인 구현(mandatory implementation)을 물려받게 하는 것 입니다.
cf. 인터페이스 상속은 구현 상속과 다릅니다. public 상속에서, 파생 클래스는 항상 기본 클래스의 인터페이스를 모두 물려받습니다.
cf. 순수 가상 함수는 인터페이스 상속 만을 허용합니다.
cf. 단순(비순수) 가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정합니다.
cf. 비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속도 가하도록 지정합니다.
[항목 35] 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
* 비가상 인터페이스 관용구를 통한 템플릿 메서드 패턴
> 가상 함수는 반드시 private 멤버로 두어야 한다.
> 공개되지 않은 가상 함수를 비가상 public 멤버 함수로 감싸서 호출한다.
> 비가상 함수 인터페이스(non-virtual interface : NVI) 관용구 : 템플릿 메서드 패턴(Template Method Pattern)
> NVI 관용구에 쓰이는 비가상 함수를 가상 함수의 랩퍼
* 함수 포인터로 구현한 전략 패턴
> 군더더기 없이 전략 패턴의 핵심만을 보여 준다.
* tr1::function으로 구현한 전략 패턴
> 함수호출성 객체(callable entity) 사용한다.
* "고전적인" 전략 패턴
> 한 쪽 클래스 계통에 속해 있는 가상 함수를 다른 쪽 계통에 속해 있는 가상 함수로 대체한다. (전방 선언 -> 다른 담당 객체의 포인터를 가져와서 포인터에서 연산)
cf.가상 함수 대신에 쓸 수 있는 다른 방법으로 NVI 관용구 및 전략 패턴을 들 수 있습니다. 이 중 NVI 관용구는 그 자체가 템플릿 메서드 패턴의 한 예 입니다.
cf.객체에 필요한 기능을 멤버 함수로부터 클래스 외부의 비멤버 함수로 옮기면, 그 비멤버 함수는 그 클래스의 public 멤버가 아닌 것들을 접근 할 수 없다는 단점이 생깁니다.
cf. tr1::function 객체는 일반화된 함수 포인터처럼 동작합니다. 이 객체는 주어진 대상 시그너처와 호환되는 모든 함수호출성 개체를 지원합니다.
[항목 36] 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
* 정적 바인딩(static binding) - 비가상 함수 (포인터의 타입에 따라 함수를 호출)
* 동적 바인딩(dynamically binding) - 가상 함수 (객체에 따라 함수를 호출)
* 바인딩 : 프로그램 소스에 쓰인 각종 내부 요소, 이름, 식별자들에 대해 값 혹은 속성을 확정하는 과정. 이 과정이 빌드 중에 이루어지면 정적 바인딩이라 하고, 실행 중에 이루어지면 동적 바인딩이라고 한다. 한편, C++의 가상 함수의 바인딩은 문서상으로는 동적 바인딩을 되어 있으나, 구현상으로는 런타임 성능을 높이기 위해 정적 바인딩을 쓰고 있다. 즉, 컴파일 중에 아예 가상 함수 테이블을 파생 클래스에 맞게 바꿈으로써, 겉보기에는 파생 클래스 타입에서 오버라이드한 가상 함수를 호출하는 것처럼 보이게 만드는 것이다.
cf. 상속받은 비가상 함수를 재정의 하는 일은 절대로 하지 맙시다.
[항목 37] 어떤 함수에 대해서도 상속 받은 기본 매개변수 값은 절대로 재정의 하지 말자
* 가상 함수는 동적으로 바인딩 되지만, 기본 매개변수 값은 정적으로 바인딩
> 정적 바인딩 : 선행 바인딩(early binding)
> 동적 바인딩 : 지연 바인딩(late binding)
* 객체의 정적 타입(static type) : 프로그램 소스 안에 선언문을 통해 객체가 갖는 타입
* 객체의 동적 타입(dynamic type) : 현재 그 객체가 진짜로 무엇이냐에 따라 결정되는 타입
cf. 상속 받은 기본 매개 변수 값은 절대로 재정의 해서는 안 됩니다. 왜냐하면 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수(여러분이 오버라이드 할 수 있는 유일한 함수이죠) 동적으로 바인딩 되기 때문입니다.
[항목 38] "has-a(...는 ...를 가짐)" 혹은 "is-implemented-in-terms-of(...는 ...를 써서 구현됨)"를 모형화 할 때는 객체 합성을 사용하자
* 합성 : 어떤 타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계를 일컫음
= 레이어링(layering), 포함(containment), 통합(aggregation), 내장(embedding) 등...
* 응용 영역 - 객체 중에 우리 일상생활에서 볼 수 있는 사물을 본 뜬 것들
* 구현 영역 - 응용 영역에 속하지 않는 시스템 구현만을 위한 인공물
cf. 객체 합성(composition)의 의미는 public 상속이 가진 의미와 완전히 다릅니다.
cf. 응용 영역에서 객체 합성의 의미는 has-a(...는 ...를 가짐)입니다. 구현 영역에서는 is-implemented-in-terms-of(...는 ...를 써서 구현됨)의 의미를 갖습니다.
[항목 39] private 상속은 심사숙고해서 구사하자
* 상속 관계가 private이면 컴파일러는 일반적으로 파생 클래스 객체를 기본 클래스 객체로 변환 하지 않는다.
* 기본 클래스로부터 물려받은 멤버는 파생 클래스에서 모조리 private 멤버가 된다.
* 공백 클래스 : 비정적 데이터 멤버가 없는 클래스
(가상함 수와 가상 기본 클래스가 없어야 함)
- 개념적으로 차지하는 메모리 공간이 없어야 한다.
- 기술적인 우여곡절 끝에서... C++에서는 독립 구조(freestanding)의 객체는 반드시 크기가 0을 넘어야 한다는 것이 정해 저 있음 -> 멤버가 아무것도 없어도 1byte 크기
- 바이트 정렬(byte alignment)이 필요하다고 판단되면 컴파일러는 바이트 패딩(padding) 과정을 추가 할 수 있어서 공백 클래스 객체를 다른 객체의 멤버로 이용할 경우 1byte 이상의 크기가 될 수 있음
* 공백 기본 클래스 최적화(empty base optimization : EBO)
- 공백 클래스를 멤버로 넣지 않고 상속 시키면 독립구조 객체가 안되기 때문에 공백 클래스에 대한 크기가 0이 된다.
- 단일 상속하에만 적용된다. C++ 객체의 레이아웃을 결정하는 규칙이 일반적으로 이러저러한 관계로, 기본 클래스를 두 개 이상 갖는 파생 클래스에는 적용될 수 없다.
* 실무적인 입장에서, 공백 클래스는 진짜로 텅 빈 것은 아니다.
- typedef, enum, 정적 데이터 멤버, 비가상함수 등...
cf. private 상속의 의미는 is-implemented-in-terms-of(...는 ...를 써서 구현됨)입니다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지만, 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우 혹은 상속 받은 가상 함수를 재정의 해야 할 경우에는 private 상속이 나름대로 의미가 없습니다.
cf. 객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO)를 활성화 시킬 수 있습니다. 이 점은 객체 크기를 가지고 고민하는 라이브러리 개발자에게 꽤 매력적인 특징이되기도 합니다.
[항목 40] 다중 상속은 심사숙고해서 사용하자
* 가상 상속(virtual inheritance)
- 상속되는 데이터 멤버의 중복 생성을 막기 위해 가상 상속을 사용하는 클래스로 만들어진 객체는 가상 상속을 쓰지 않는 것보다 일반적으로 크기가 더 크다.
- 가상 기본 클래스의 데이터 멤버에 접근하는 속도도 비가상 기본 클래스의 데이터 멤버에 접근하는 속도보다 느리다.
- 가상 상속은 비싸다.
* 가상 상속 초기화 규칙
- 초기화가 필요한 가상 기본 클래스로부터 클래스가 파생된 경우, 이 파생 클래스는 가상 기본 클래스와의 거리에 상관 없이 가상 기본 클래스의 존재를 염두에 두고 있어야 한다.
- 기존의 클래스 계통에 파생 클래스를 새로 추가 할 때도 그 파생 클래스는 가상 기본 클래스(역시 거리에 상관 없이)의 초기화를 떠맡아야 한다.
cf. 다중 상속은 단일 상속보다 확실히 복잡합니다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해질 수도 있습니다.
cf. 가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 및 대입 연산의 복잡도가 커집니다. 따라서 가상 기본 클래스에는 데이터를 두지 않는 것이 현실적으로 가장 실용적입니다.
cf. 다중 상속을 적법하게 쓸 수 있는 경우가 있습니다. 여러 시나리오 중 하나는, 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것입니다.
7. 템플릿과 일반화 프로그래밍
[항목 41] 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터
* 객체 지향 프로그래밍의 세계를 회전시키는 축
- 명시적 인터페이스(explicit interface) & 런타임 다형성(runtime polymorphism)
* 템플릿과 일반화 프로그래밍
- 암시적 인터페이스(implicit interface) & 컴파일 타임 다형성(compile-time polymorphism)
cf. 클래스 및 템플릿은 모두 인터페이스와 다형성을 지원합니다.
cf. 클래스의 경우, 인터페이스는 명시적이며 함수의 시그너처를 중심으로 구성되어 있습니다.
다형성은 프로그램 실행 중에 가상 함수를 통해 나타납니다.
cf. 템플릿 매개변수의 경우, 인터페이스는 암시적이며 유효 표현식에 기반을 두어 구성됩니다.
다형성은 컴파일 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타납니다.
[항목 42] typename의 두 가지 의미를 제대로 파악하자
* template<class T> class Widget; // template<class T> class Widget; 둘 다 같다.
* 의존 이름(dependent name) : 템플릿 내의 이름 중에 템플릿 매개변수에 종속된 것
* 중첩 의존 이름(nested dependent name) : 의존 이름이 어떤 클래스 안에 중첩
- 컴파일러가 구문분석을 할 때 힘들어짐
* 템플릿 안에서 중첩 의존 이름을 참조 할 경우에는, 이름 앞에 typename 키워드 사용
* typedef typename [A] [B] 가능
cf. 템플릿 매개변수를 선언할 때, class 및 typename은 서로 바꾸어 써도 무방합니다.
cf. 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename을 사용합니다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외입니다.
[항목 43] 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
* 완전 템플릿 특수화(total template specialization)
- 템플릿이 어떤 타입에 대해 특수화 됐고, 이 템플리스이 매개 변수들이 하나도 빠짐 없이 구체적인 타입으로 정해진 상태
* 템플릿 클래스에서 기본 클래스의 함수를 호출
- 기본 클래스 함수에 대한 호출문 앞에 "this->"를 붙임
- using 선언을 사용
- 호출 할 함수가 기본 클래스의 함수라는 점을 명시적으로 지정
> 가상 함수 바인딩이 무시 되므로 추천 하지 않음
* 기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 미리(파생 클래스 템플릿의 정의가 구문분석될 때) 들어가느냐, 아니면 나중에(파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화 될 때) 들어가느냐가 중요함
- C++에서 템플릿은 '이른 진단(early diagnose)'을 선호하는 정책으로 결정
- 파생 클래스가 템플릿으로부터 인스턴스화 될 때 컴파일러가 기본 클래스의 내용에 대해 아무것도 모르는 것으로 가정
cf. 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는, "this->"를 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 써 주는 것으로 해결합시다.
[항목 44] 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
* 코드 비대화(code bloat)
* 공통성 및 가변성 분석(commonality and variability analysis)
* 작업 세트(working set) : 한 프로세스에서 자주 참조하는 페이지의 집합. 실질적으로 이 페이지 집합은 늘 주 메모리에 올라와 있기 때문에, 한 프로세스에서 주 메모미에 올라갈 수 있는 페이지의 양을 나타낸다. 간단히 '프로세스가 현재 사용하는 메모리 양'을 지칭할 때도 쓴다.
* 참조 지역성(locality of reference) : 프로세스의 메모리 참조가 실행 중에 균일하게 흩어져 있지 않으며 특정 시점 및 특정 부분에 집중 된다는 경험적/실험적 특성. 여기에는 시간적 지역성(temporal locality : 지금 참조된 메모리는 멀지 않은 나중에 또 참조될 가능성이 높다)는 성질과 공간적 지역성(spatial locality: 지금까지 참조되 ㄴ메모리와 가까운 곳에 있는 메모리가 참조될 가능성이 높다는 성질)의 두가지 지역성이 있다. 캐시는 바로 이러한 참조 지역성을 이용해서 수행 성능을 높이는 고속 메모리이다.
cf. 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어 집니다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 됩니다.
cf. 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있습니다.
cf. 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화 되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로 써 비대화를 감소시킬 수 있습니다.
[항목 45] "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방!
* 스마트 포인터는 일반 포인터 처럼 암시적 변환을 할 수 없음
* 멤버 함수 템플릿(member function template, 멤버 템플릿) : 어떤 클래스의 멤버 함수를 찍어내는 템플릿
* 일반화 복사 생성자(generalized copy constructor)
cf. 호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용합시다.
cf. 일반화 된 복사 생성 연산과 일반화 된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복새 대입 연산자는 여전히 직접 선언해야 합니다.
[항목 46] 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
cf. 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면, 이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의 합시다.
[항목 47] 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자
* 특성정보(traits) : 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체
* 특성정보 클래스
template<typename IterT>
struct iterator_traits;
* 부분 템플릿의 특수화(partial template specialization)
cf. 특성 정보 클래스는 컴파일 도중에 사용 할 수 있는 타입 관련 정보를 만들어냅니다. 또한 특성 정보 클래스는 템플릿 및 템플릿 특수 버전을 사요하여 구현합니다.
cf. 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if...else 점검문을 구사 할 수 있습니다.
[항목 48] 템플릿 메타프로그래밍, 하지 않겠는가?
* 템플릿 메타프로그래밍(template metaprogramming : TMP)
- TMP를 쓰면 다른 방법으로는 까다롭거나 불가능한 일을 굉장히 쉽게 할 수 있다
- 템플릿 메타프로그램은 C++ 컴파일이 진행되는 동안 실행되기 때문에, 기존 작업을 런타임 영역에서 컴파일 타임 영역으로 전환 할 수 있다.
* typeid 연산자는 특성정보를 쓰는 방법보다 효율이 떨어진다.
- 타입 점검 동작이 컴파일 도중이 아니라 런타임에 일어남
- 런타임 타입 점검을 수행하는 코드는 실행 파일에 들어가야 함
* 튜링 완전성 : 범용 프로그래밍 언어처럼 어떤 것이든 계산할 수 있는 능력을 갖고 있음
* 재귀식 템플릿 인스턴스화(recursive template instantiation) :
- 나열자 둔갑술(enum hack)으로 처리, 템플릿 인스턴스화 버전마다 자체적으로 사본을 갖게 되고, 각각 루프 돌 때 마다 내부 변수의 값이 담겨짐
> Factorial 함수
template<unsigned n>
struct Factorial{
enum {value = n * Factorial<n-1>::value};
};
template<>
struct Factorial<0>{
enum {value = 1};
};
> 팩토리얼 만들 때 n 부터 0까지 만들어져 있게 됨
* Cpp에서 TMP가 유용한 부분
- 치수 단위의 정확성 확인
- 행렬 연산의 최적화
> 표현식 템플릿(expression template) : 임시 객체를 없앨 뿐만 아니라 루프까지 합칠 수 있음
- 맞춤식 디자인 패턴 구현의 생성
> 정책 기반 설계(policy-based design)
> 생성식 프로그래밍(generative programming)
cf. 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환 하는 효과를 냅니다. 따라서 TMP를 쓰면 선행 에러 탐지와 높은 런타임 효율을 손에 거머쥘 수 있습니다.
cf. TMP는 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있으며, 또한 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 쓸 수 있습니다.
8. new와 delete를 내 맘대로
- 다중 스레드 환경에서 힙(heap)은 수정이 가능한 전역 자원으로 분류
> 스레드 들이 이런 전역 자원에서 미친 듯이 접근하면서 경쟁 상태가 생길 소지가 많음
> 수정 가능한 정적 데이터를 이용시 적절한 동기화를 걸지 않으면, 스레드 잠금에 영향을 받지 않는 어떤 알고리즘을 쓰든, 동시 접근을 막는 어떤 세심한 설계를 적용하든, 소용없음
- operator new 및 operator delete는 객체 한 개를 할당할 때만 적용되는 함수
> 배열을 담을 메모리의 경우, 할당할 때는 operator new[]를 쓰고 해제할 때는 operator delete[]를 쓴다.
[항목 49] new 처리자의 동작 원리를 제대로 이해하자
cf. set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출 되는 함수를 지정 할 수 있습니다.
cf. 예외불가(nothrow) new는 영향력이 제한 되어 있습니다. 메모리 할당 자체에만 적용되기 때문입니다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있습니다.
[항목 50] new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자
* 바이트 정렬(byte alignment)
- 아키텍처(architecture)적으로 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다. 포인터는 4의 배수에 해당하는 주소에 맞추어 저장되어야(다시 말해 4바이트 단위로 정렬)하거나 double 값은 8의 배수에 해당하는 주소에 맞추어 저장되어야(즉, 8바이트 단위로 정렬) 한다. 어떤 아키텍처의 경우 바이트 정렬 제약을 따르지 않으면 하드웨어 예외를 일으킬 수 있고 보다 느슨한 제약을 두는 아키텍처도 있지만 이런 경우 바이트 정렬을 만족한다면 더 나은 성능을 제공하여 보답한다. 대표적인 예가 인텔x86 아키텍처인데, 어떤 바이트 단위에 맞추더라도 double값을 정렬할 수 있지만 8바이트 단위로 정렬하면 런타임 접근 속도가 훨씬 빨라진다.
cf. 개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데 는 여러 가지 나름대로 타당한 이유가 있습니다. 여기에는 수행 성능을 향상 시키려는 목적, 힙 사용 시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함됩니다.
[항목 51] new 및 delelte를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자
cf. 관례적으로, operator new 함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 합니다. 클래스 전용 버전은 자신이 할당하리로 예정된 크기보다 더 큰(틀린) 메모리 블록에 대한 요구도 처리해야 합니다.
cf. operator delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 합니다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 합니다.
[항목 52] 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자
cf. operator new 함수의 위치지정(placement) 버전을 만들 때는, 이 함수와 짝을 이루는 위치지정 버전의 operator delete 함수도 꼭 만들어 주세요, 이 일을 빼먹었다가는, 찾아내기도 힘들며 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험하게 됩니다.
cf. new 및 delete의 위치지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의해 주세요.
9. 그 밖의 이야기들
[항목 53] 컴파일러 경고를 지나치지 말자
[항목 54] TR1을 포한한 표준 라이브러리 구성요소와 편안한 친구가 되자
[항목 55] Boo자유친! 부스트를 늘 여러분 가까이에
'programing > C++' 카테고리의 다른 글
형변환 연산자 오버로딩 (0) | 2016.12.06 |
---|---|
explicit (0) | 2016.12.05 |
메모리 누수 확인 (0) | 2016.10.12 |
auto (0) | 2016.10.12 |
std::tuple (0) | 2016.10.12 |