0장
- 생성자를 explicit로 선언하기
- 암시적 타입 변환을 이용할 여지를 남기는 게 아니면 넣어주자.
- explicit가 아닌 것들과 비교할 때 유용하다.
- 미정의 동작
- 정의되어 있지 않은 동작을 시도하면 어떤 일이 생길지 예측 불가능하다.
- 미정의 동작과 멀리 떨어져 가는 코드를 만들기 위해 최선을 다하자!
- 인터페이스
- 구현은 없고 뼈대만 있는 클래스. 모든 함수가 순수 가상 함수인 클레스인 셈
- C++에는 없어서 그냥 순수 가상 함수를 만들어야 한다.
- RT1
- C++ 표준 라이브러리에 새로 추가되는 기능들에 대한 명세.
- 부스트
- 오픈소스 C++ 라이브러리를 제공하는 단체
- 교차 검증도 거쳤고, 플랫폼 간 이식성도 가지고 있다.
1장
- C++을 사용한 효과적인 프로그래밍 규칙은 어느 기능을 사용하는지에 따라 달라진다.
상수
- #define 매크로 대신 const, enum, linine를 사용하자.
- 부동소수점 실수 타입일 경우 const 상수는 #define보다 크기가 작을 수도 있다. 여러 번 써도 원본을 재사용하기 때문.
- const 를 쓸 때는 내부 데이터도 const로 보호해주자.
```const char const Temp = "Hello";```
- 클래스 맴버에 정의하는 경우 static 맴버로 만드는 게 좋다.
- 나열자(enum) 둔갑술
- 나열자를 int로 활용해서 상수 컴파일 타임에 상수 초기화를 하는 것.
const 변수는 선언과 동시에 초기화해도 컴파일 타임에 정의되지는 않으니까 배열 크기 할당같은 곳에는 못 쓰고, 그런 걸 나열자로 선언해서 사용하는 것.
- 대신 r_value 니까 참조는 못 한다.
매크로 함수
- 함수처럼 보이지만 함수 오출 오버헤드를 일으키지 않는 매크로!
- 매크로 본문에 들어 있는 인자마다 반드시 괄호를 씌워 줘야 한다.
- 그래도 문제는 생긴다.
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
낌새만 보이면 const를 붙이자.
- const 는 가능한 한 항상 사용하자.
- const int *a, int const *a : 데이터 변경 불가능
- int *const a : 참조 대상 변경 불가능
- 함수 반환값을 상수로 해주면 안전성이나 효율을 지키면서 에러를 줄이는 효과를 자주 볼 수 있다.
a == b 를 의도했지만 오타로 a = b를 쓰는 상황이라던지..
상수 맴버 함수
- const 맴버함수 알지? 내부에서 값 변경 못한다.
- 클래스의 인터페이스를 이해하기 좋게 해준다.
- 상수 객체를 사용할 수 있게 한다. 코드 효율성에서 중요하다.
- 객체 전달을 상수 객체에 의한 참조자로 진행하면 실행 성능이 늘어난다.
- const를 뺀 것만으로도 오버로딩이 가능하다.
class TextBlock
{
public:
TextBlock(std::string s) { text = s; }
~TextBlock() {}
const char& operator[](std::size_t position) const
{
std::cout << "const" << std::endl;
return text[position];
}
char& operator[](std::size_t position)
{
std::cout << "not const" << std::endl;
return text[position];
}
private:
std::string text;
};
int main()
{
TextBlock temp("WWWW");
temp[1];
const TextBlock src("QQQ");
src[1];
}
- 비트수준 상수성(물리적 상수성)
함수가 그 객체의 어떤 데이터 맴버(static 제외)도 건들지 말아야 함
- 논리적 상수성
상수 멤버 함수라고 해서 객체의 한 비트도 수정 못하는게 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 해도 상수 멤버이다는 주장.
mutable 같은거 써서 수정해버린다...
3장 자원 관리
- 자원 관리는 직접 하다 보면 언젠가 실수 한다.
- 객체를 활용하여 자원 관리를 하자 (스마트 포인터같은)
- 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
- 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
범위를 벗어나거나 소멸될 때 소멸자는 자동으로 호출되니까 중간에 블록을 빠져나가도 소멸자를 통한 자원 해제는 된다.
- 스마트 포인터를 RCSP(Reference Counting Smart Pointer)라고 부른다.
- 스마트 포인터는 동적 할당된 배열을 위한 포멧이 없다. (STL컨테이너 쓰라고)
- 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자.
- RAII: C++의 자원 관리 설계 패턴
- 프로그래머가 직접 자원을 획득하고 관리하는 것이 아니라, 자원의 생성, 파괴, 관리를 모두 객체에 위임한다.
- 프로그래머는 자원 관리를 망각해도 상관 없다.(Fire and Forget)
- 복사를 금지한다(일반적으로).
- 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
- 관리하고 있는 자원을 진짜로 복사한다 (사본이 아니라 별개의 객체가 되도록).
- 관리하고 있는 자원의 소유권을 옮긴다.
- 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자.
- RAII 클래스를 실제 자원으로 바꾸고 싶다면
명시적 변환을 오버라이딩 하거나
암시적 변환을 허용하는 방법이 있겠다.
둘 다 장단점이 있으니까 상황에 맞게 잘 써야겠지?
- new 및 delete를 사용할 때는 형태를 반드시 맞추자
- new에 []를 썼으면 할당 해제는 delete[]를 사용해야 한다.
- new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자.
foo(shared_ptr<CObj>(new CObj));
shared_ptr<CObj> Temp(new CObj);
foo(Temp);
- 책이 잘못 나온거같은데?
- make_shared 쓰는게 제일 안전하고 사실 new를 shared_ptr에 넣는 것 자체가 위험함.
4장 설계 및 선언
- 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자.
- 사용자가 저지를 만한 실수들을 머리에 넣어두어야 한다.
- 새로운 타입을 들여와 인터페이스를 강화하면 많은 실수를 막을 수 있다.
거기에 제약까지 걸어두면 더 좋다.
- 정당한 이유가 없다면 사용자 정의 타입은 기본 타입처럼 동작하게 만들어라.
인터페이스는 일관성이 좋아야 한다.
- 사용자 가 뭔가 외워야 제대로 쓸 수 있는 인터페이스는 실수하기 쉽다.
- 메모리 해제를 사용자의 책임으로 두지 말고 자동으로 되게 하자.
- 그냥 뭐 복잡하면 자동으로 되게 해라.
- 클래스 설계는 타입 설계와 똑같이 취급하자.
- 좋은 클래스란
- 문법이 자연스럽다.
- 의미구조가 직관적이다.
- 효율적인 구현이 한 가지 이상 가능해야 한다.
효과적인 클래스 설계를 위해 고려할 것
- 새로 정의한 타입의 객체 생성/소멸은 어떻게 이루어져야 하는가?
- 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
- 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우 어떤 의미를 줄 것인가?
- 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
- 클래스의 일부 데이터 멤버의 몇 가지 조합값은 반드시 유효해야 한다.(불변속성)
- 생성자나 setter 함수들은 예외처리를 확실하게 해 주는게 좋다.
- 기존의 클래스 상속 계통망에 맞출 것인가?
- 어떤 종류의 타입 변환을 허용할 것인가?
- 어떤 연산자와 함수를 두어야 의미가 있을까?
- 표준 함수들 중 어떤 것을 접근 허용하지 말 것인가?(private)
- 새로운 타입의 멤버에 대한 접근권한을 얼마나 줄 것인가?
- '선언되지 않은 인터페이스'로 무엇을 둘 것인가?
- 어떤 기능을 보장할까? 성능, 예외안전성, 메모리 안전 이런걸 보장할수록 구현에 제약이 생긴다.
- 새로 만드는 타입이 얼마나 일반적인가?
- 잔뜩 만들어야 된다면 클래스 템플릿을 만들어야겠는걸?
- 정말로 꼭 필요한 타입인가?
- call by value 보다는 call by reference가 보통은 좋다.
왠지 알지?
- 원시 타입이나 이터레이터, 함수 객체 타입에는 비추천 (특히 이터레이터는..)
- 함수에서 객체를 반환해야 할 경우에 포인터나 참조자를 반환하려고 들지 말자
- 지역 스택 객체는 해당 함수를 벗어날 떄 원본이 삭제될 수 있다.
- 이럴 때는 새로 생성한 객체를 반환하게 하자.
- C++17 부터는 rvo 강제라서 그냥 객체 return 해도 복사마저 안 일어난다.
- 데이터 멤버가 선언될 곳은 private 영역이다.
- 문접적 일관성이 유지된다.
- 데이터 은닉과 보호(캡슐화)에 필요하다.
- 함수 호출을 통해 데이터를 수정하게 함으로써 유효성 검사 등을 같이 하는 등 더 안전한 작업의 기회가 주어진다.
- 멤버 함수보다는 비멤버 비프렌드 함수를 사용하자.
- 클래스 밖에서 객체의 함수를 호출하는 식으로 해서 작업 과정이 너무 은닉되지 않게 해주자.
- 클래스의 인터페이스가 명확해지고 캡슐화 수준을 조절할 수 있게 된다.
- 그렇다고 private 데이터를 다 빼서 쓰면 안되니까 비프랜드 함수를 사용한다.
- 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자.
- 연산자 오버라이딩을 통해 CNum * 2 는 되는데 2 * CNum은 안 되는 경우를 생각해보면 이해가 된다.
- 모든 매개변수에 대해 암시적 타입 변환이 필요하다면, 그 함수는 비멤버 함수여야 한다.
- C++은 첫 번째 인자인 this 에 대해 암시적 변환이 일어나지 않는다.
그걸 원한다면 비멤버 함수로 정의해야 한다.
- 예외를 던지지 않는 swap 에 대한 지원도 생각해 보자.
- swap 함수는 복사를 세번이나 하는데 사용자 정의 타입은 무겁다.
- 표준에서 제공하는 swap이 쓸 만 하면 걍 써라.
- 좀 무겁다면
- public으로 swap을 선언해서 구현해라. 절대 예외를 던지면 안 된다.
- 클래스 혹은 템플릿이 들어 있는 네임스페이스와 같은 네임스페이스에 비맴버 swap을 만든다. 그리고 1에서 만든 swap 멤버 함수를 이 비멤버 함수가 호출하게 만든다.
- 새로운 클래스를 만들고 있다면, 그 클래스에 대한 swap의 특수화 버전을 준비해 둔다. 그리고 이 특수화 버전에서도 swap 멤버 함수를 홏출하도록 만든다.
- 사용자 입장에서 swap을 호출할 때 std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출하자.
- 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는건 가능하지만 std에 어떤 것이라도 새로 추가하려고 들지 마라.
5장 구현
- 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자.
- 쓰지도 않을 변수를 생성하는건 낭비다. 특히 객체면 비용도 발생한다.
- 어떤 변수를 사용해야 할 때가 오기 전까진 그 변수의 정의를 늦춰야 한다.
- 초기화 인자를 손에 넣기 전까지 정의를 늦출 수 있는지도 확인해야 한다.
- 캐스팅은 최대한 절약하자.
- 컴파일 단계에서 오류를 잡아주지 못한다.
- 캐스팅이 필요하다면 함수 안에 숨기는 걸 권장한다. 최소한 사용자는 캐스팅 안 할 수 있도록
- 파생 클래스의 함수를 호출하고 싶은데, 그 객체를 조작할 수단이 부모 클래스의 포인터밖에 없는 경우라면
- 파생 클래스 객체에 대하 포이터를 컨테이너에 담아둠으로써 각 객체를 기보 클래스 인터페이스를 통해 조작할 필요를 없앤다.
- 원하는 함수를 가상 함수의 형태로 기본 클래스에 넣어둔다.
- 절대 하면 안되는거: 폭포식 dynamic_cast라고 하는데, 분기문을 잔뜩 넣고 분기에 따른 dynamic_cast를 하는거
- 내부에서 사용하는 객체에 대한 핸들을 반환하는 코드는 되도록 피하자.
private 맴버였더라도 참조를 반환해버리면 외부에서 마음대로 수정할 수 있다.
- 클래스 데이터 멤버는 아무리 숨겨도 그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다.
- 어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면, 이 함수의 호출부에서 그 데이터의 수정이 가능하다.
- 멤버 함수의 핸들 또한 반환하지 말아야 한다.
public이 아닌 멤버 함수의 포인터를 반환하는 멤버 함수를 만들면 안된다. 접근 수준이 바뀌는 셈이니까.
- 그러니까 반환타입에
const를 붙여주면 된다.
- 애초에 핸들을 외부로 반환 한 상황이면 핸들이 객체보다 수명이 길 수 있기 때문에 그냥 위험하다.(무효참조 핸들, 댕글링 포인터)
- 예외 안정성을 확보하기 위해 노력하자
- 예외 안정성을 가진 함수라면 예외처리를 이렇게 해야 한다.
- 자원이 새도록 만들지 않는다. (메모리를 객체로 관리하기 알지?)
- 자료구조가 더럽혀지는 것을 허용하지 않는다.
- 다음 세 가지 보장 중 하나는 반드시 제공해야 한다.
- 기본적인 보장: 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장.
- 강력한 보장: 함수 동작 중에 예외가 발생하면, 호출이 없었던 것처럼 프로그램의 상태가 되돌아간다는 보장.(copy and swap 같은 방식)
- 예외불가 보장: 예외를 절대로 던지지 않겠다는 보장(그게 가능해?).
- 보장이 없는 코드가 존재해야 한다면 알아볼 수 있게 표시해서 인수인계가 파악할 수 있게 하자.
- 인라인 함수는 미주알고주알 따져서 이해해 두자.
- 인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해진다.
- 대부분의 경우 컴파인을 컴파일 도중에 수행하기 때문에 인라인 함수는 대체적으로 헤더 파일에 들어 있어야 한다.
- 컴파일러가 함수의 형태를 알아야 하니까
- 템플릿도 같은 이유고 헤더 파일에 들어 있는게 맞다.
- 인라인 함수는 virtual 선언을 하지 않는다. 어떤 함수를 호출해야 하는지 컴파일 단계에서 모르니깐.
- 가급적이면 단순한 함수만 인라인으로 선언하자. 길명 디버깅도 잘 안된다.
- 파일 사이의 컴파일 의존성을 최대로 줄이자.
- #include 문은 정의한 파일과 위의 헤더 파일 사이에 컴파일 의존성을 엮는다.
하나라도 변경점이 생기면 include 해둔 헤더들은 전부 빌드에 끌려간다.
- 정의부에 대한 의존성을 선언부에 대한 의존성으로 바꾸자.
헤더에서는 전방 선언 정도만 하고 include 는 cpp에서 하라는것.
- 라이브러리 헤더는 그 자체로 모든 것을 갖추어야 하며 선언부만 갖고 있는 형태여야 한다. 이 규칙은 템플릿 유무에 관계없이 동일하게 적용해야 한다.
6장 상속 그리고 객체 지향 설계
- public 상속 모형은 반드시 is-a 를 따르도록 만들자.
- 너무 포괄적으로 상속시켜서 자식에 따라 사용하면 안 되는 기능을 상속받는 일이 없도록 분리하자.
- 하지 못하게 할 함수는 컴파일 타임에 막을 수 있는 게 좋다.
- 상속된 이름을 숨기는 일은 피하자.
- 더 안쪽 스코프 또는 자식 클래스에 같은 이름의 변수가 선언되면 바깥의 이름은 가려진다.
- 이 가려지는 문제는 안쪽 스코프에서 using 으로 이름을 명시해주면 된다.
이러면 virtual 오버로딩도 가능해진다!
- 부모의 함수를 감싸는 전달 함수를 만드는 것도 방법이다.
- 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자.
- 멤버 함수 인터페이스는 항상 상속되게 되어 있다.
- 구현이 붙은 순수 가상 함수를 호출하려면 반드시 클래스 이름을 한정자로 붙여 주어야 한다.
- 클래스 설계에서 흔히 발생하는 실수
- 무지성으로 모든 멤버 함수를 비가상 함수로 선언하는 것.
가상 함수 써도 비용 크게 안 다르니까 시원하게 써라
- 무지성으로 모든 멤버 함수를 가상 함수로 선언하는 것.
재정의가 필요 없다면 비가상 함수로 쓰자.
- 가상함수 대신 사용할 수 있는 대체제도 많다.
- NVI(비가상 함수 인터페이스)
- 인터페이스는 비가상 함수로 제공
- 하위 클래스에서 override할 부분은 protected virtual 함수로 분리
- Template Method 패턴의 변형
- 사저 동작과 사후 동작을 넣을 수 있다.
- 함수포인터 및 function 객체 사용
- 함수 호출성 개체 보유
- 상속받은 비가상 함수를 파생 클래스에서 재정의하면 안된다
- 동적 바인딩이 안 돼서 호출 안 되는 경우가 많이 생긴다.
- 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자.
- 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수는 동적으로 바인딩되기 때문이다.
- has-a 또는 is-implemented-in-terms-of 는 객체합성을 쓰자.
- public과 객체합성의 조합은 합성은 가능하게 하되 자식이 부모의 멤버함수를 재정의할 수 없도록 막고 싶을 때 사용할 수 있다.
- private 상속은 심사숙고해서 구사하자.
- 파생 클래스 쪽에서 기본 클래스의 Protected 멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의해야 할 경우 private 상속이 나름 의미가 있다.
- 객체 합성과 달리 공백 기본 클래스 최적화(EBO)를 활성화시킬 수 있다. 이 점은 객체 크기를 가지고 고민하는 라이브러리 개발자에게 꽤 매력적인 특징이 되기도 한다.
EBO: 불필요한 메모리 낭비를 줄이기 위한 최적화 기법
빈 클래스가 원래는 최소 1바이트를 차지하는데, 컴파일러가 빈 클래스는 크기를 0으로 만들어준다.
- 템플릿 라이브러리 내부 구현에서 자주 쓰인다.
- 빈 클래스를 왜 써?: 태그 용도, 타입 분기, 템플릿 특수화 등에 쓴다.
- 다중 상속은 되도록 조심히 사용하자.
- 다이아몬드 구조를 만들 위험이 있다.
7장. 템플릿과 일반화 프로그래밍
- 암시적 인터페이스와 컴파일 타임 다형성.
- 객체 지향 프로그래밍의 핵심 개념은 명시적 인터페이스와 런타임 다형성이다.
- 명시적 인터페이스: 소스 코드에 명시적으로 드러나는 인터페이스
- 런타임 다형성: 함수에 대한 실제 호출이 프로그램 실행 중(런타임) 결정된다.
- 실제로 많이 사용하는 건 암시적 인터페이스와 컴파일 타임 다형성이다.
- 암시적 인터페이스: 명시적으로 인터페이스를 상속하지 않아도, 필요한 함수나 멤버만 구현돼 있으면 그 인터페이스처럼 동작할 수 있는 방식
- 컴파일 타임 다형성: 오버로딩, 템플릿 인스턴스화, 인라인화 등 컴파일 타임에 필요한 코드가 생성되는 특성.
- typename의 두 가지 의미를 제대로 파악하자
- 템플릿의 타입 매개변수를 선언할 때는 class 와 typename의 의미가 동일하다.
- 의존 이름: 템플릿 내의 이름 중에 템플릿 매개변수에 종속된 타입.
- 의존 이름이 어떤 클래스 안에 중첩되어 있는 경우 중첩 타입 의존 이름이라고 한다.
- 모호성을 해결하기 위해 컴파일러는 템플릿 안에서 중첩 의존 이름을 만나면 타입을 명시하지 않는 한 타입이 아니라고 가정한다.
- 그러니까 중첩 의존 이름은 기본적으로 타입이 아니라고 가정한다.
- 타입이라는거 명시하려면 앞에 typename를 붙여야 한다.
typename c::const_iterator iter
- 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename을 사용한다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외이다.
- 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자.
- 템플릿 안에서 컴파일러는 기본 클래스에 대해 모른다.
- 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는
this->를 접두사로 붙이거나, 기본 클래스 한정문을 명시적으로 써 주어야 한다.
- 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자.
- 코드 비대화에 주의하자.
- 공통성 및 가변성 분석을 사용하자.
- 두 함수를 분석해서 공통적인 부분과 다른 부분을 찾은 뒤 공통 부분은 새로운 함수에 옮기고, 다른 부분은 원래의 함수에 남겨두자.
- 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있다.
- 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.
- 호환되는 모든 타입을 받아들이는 멤버 함수를 만드려면 멤버 함수 템플릿을 사용하자.
- 일반화 복사 생성 연산과 일반화된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 한다.
- 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자.
- 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면, 이런 함수는 템플릿 안에 프랜드 함수로서 정의하자.
- 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자.
- STL 반복자는 각 반복자가 지원하는 연산에 따라 다섯 개의 범주로 나뉜다.
- 입력 반복자: 한 칸씩 전진만 가능, 한 번 읽기만 가능
- 출력 반복자: 비슷하지만 한 번 쓰기만 가능하다.
- 순방향 반복자: 입출력 반복자가 하는 건 다 가능하고, 자신이 가리키고 있는 위치에서 읽기과 쓰기를 동시에 여러 번 할 수 있다.
- 양방향 반복자: 순뱡향 반복자에 뒤로 가는 기능을 추가함.
- 임의 접근 반복자: 양방향 반복자에 반복자 산술 연산 수행 기능을 추가한 것.
- 특설정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어낸다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.
- 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if_else 점검문을 구사할 수 있다.
- 템플릿 메타프로그래밍(TMP) 사용해 보자.
- 다른 방법으로는 까다롭거나 불가능하 일을 굉장히 쉽게 할 수 있다.
- 컴파일 타임에 실행된다.
- 런타임에 작동하기 때문에 선행 에러 탐지와 높은 런타임 효율을 낼 수 있다.
- 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있다.
- 특정 타입에 때해 부적절한 코드가 만들어지는 것을 막는 데도 사용한다.
8장. new 와 delete를 내 맘대로
- new 처리자의 동작 원리를 제대로 이해하자.
- new 작업을 실패하면 new 처리자 라고 하는 예외 처리 함수를 우선적으로 호출하도록 되어 있다.
- new 처리자 함수가 프로그램의 동작에 좋은 영향을 미치는 쪽으로 설계되어 있다면 다음 동작 중 하나를 꼭 해주어야 한다.
- 사용할 수 있는 메모리를 더 많이 확보한다.
프로그램이 시작될 때 메모리 블록을 크게 하나 할당해 둔다던지 하는 방식.
- 다른 new 처리자를 설치한다.
- 널 포인터를 넘겨서 new 처리자의 설치를 제거함.
- bal_alloc에 파생되는 예외를 던진다
- exit 등을 호출해서 복귀하지 않는다.
- new 및 delete를 언제 바꿔야 좋을지 파악해 두자.
- 예외처리를 넣어서 잘못된 힙 사용을 탐지하기 위해
- 오버런/언더런 버그 탐지용 바이트 패턴을 적어두게 만들 수 있겠다.
- 효율을 향상시키기 위해
- 메모리 풀링, 프레임 단위 할당 등을 넣어둘 수 있겠다.
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당 및 해제 속력을 높이기 위해
- free()는 내부적으로 이중 연결 리스트를 쓰기 때문에 오버헤드가 큼.
- 고정 크기 객체라면 간단한 LIFO리스트를 통해 O(1)할당/해제가 가능.
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
- 일반 malloc은 메타데이터를 저장하므로 작은 객체에 비해 낭비가 큼
- 커스텀 관리자는 필요한 최소한의 헤더만 사용하거나 헤더 없는 메모리 관리도 가능.
- 기본 할당자의 바이트 정렬 동작을 보완하기 위해
- 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해
- 캐시 효율을 개선시키기 위해 메모리를 모아둔다.
- 그때그때 원하는 동작을 수행하도록 하기 위해
- 디버깅/릴리즈의 로그 출력 차이라던지, 특수 상황에서만 다른 할당 방식을 써야하는 경우가 있다던지..
- new 및 delete를 작성할 때 따라야 할 기존의 관계를 잘 알아 두자.
- 관례적으로 operator new 함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 한다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰 메모리 블록에 대한 요구도 처리해야 한다.
- delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 한다. 클래스 전용 버전에서는 예정 크기보다 더 큰 블록을 처리해야 함.
- 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자.
- placement new를 작성하면, 짝을 이루는 placement delete도 꼭 만들어 줘야 한다.
- 참고로 이미 할당된 메모리 영역에 객체를 생성하는 기능임
이를 뺴먹으면 찾아내기도 힘들고 생겼다가 사라지기도 하는 메모리 누수를 겪게 된다.
- new 및 delete의 위치지정 버전을 선언할 때는 의도한 바도 아닌데 표준 버전이 가려지는 일이 생기지 않도록 주의해야 한다.
- using 키워드 등으로 기본 버전을 노출시켜야 한다.