Item 26 변수 정의는 늦출수 있는데까지 늦추는 근성을 발휘하자.

생성자 혹은 소멸자를 끌고 다니는 타입(자주호출의 의미)으로 변수를 정의하면 2가지의 비용이 존재.

  1. 프로그램 제어 흐름이 변수의 정의에 닿을때, 생성자가 호출되는 비용
  2. 변수가 유효범위를 벗어날때 소멸자가 호출되는 비용

해당 객체가 정말 필요한 시점에 선언을 해야된다. 불필요한 변수에 대해 생성자와 소멸자의 호출에 필요한 처리시간을 낭비할수도 있기 때문이다.

Item 27 캐스팅은 절약, 또 절약! 잊지 말자

  • const_cast<T> 객체의 상수성을 없애는 용도
  • dynamic_cast<T> 안전한 다운캐스팅을 할때, 사용하는 캐스팅
  • reinterpret_cast<T> Pointer를 int로 바꾸는 등의 하부수준 캐스팅을 위해 만들어진 연산자. 결과는 구현환경에 의존적. (integer Type => pointer Type, pointer Type -> interger Type, Pointer Type => Another Type)
  • static_cast<T> 암시적 변환을 강제로 진행할때 사용한다.

캐스팅을 하게되면 그 해당 캐스팅의 값의 사본이 임시적으로 만들어지고, 그 사본에 대해 처리하게 된다.

문자열 비교 연산에 기반을 둔 dynamic_cast<T>는 클래스 이름을 비교하기 위해서 strcmp가 최대 4번이 호출될 가능성이 있다. 상속 깊이가 더 있거나, 다중상속이라면 그 비용은 커지게 된다.

dynamic_cast<T> 을 쓰려면 파생 클래스의 객체에 대한 포인터를 컨테이너에 담아둬서, 기본 클래스 인터페이스를 통해 조작할 필요가 없게 만든다. 또한 가상함수를 만들어서 처리하도록 한다.

  1. 다른 방법이 있다면 캐스팅은 피하고, 수행 성능이 민감하다면 dynamic_cast는 다시 생각
  2. 캐스팅이 어쩔수 없이 필요하다면 함수 안에 숨겨라, 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고, 함수를 호출하게 된다.
  3. C++ 스타일의 캐스팅을 써라, 발견하기 쉽고, 역할을 뚜렷하게 볼수 있다.

Item 28 내부에서 사용하는 객체에 대한 ‘핸들’을 반환하는 코드는 되도록 피하자.

핸들

다른 객체에 손을 댈수 있게 만드는 매개제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 값에 의한 전달보다 참조에 의한 전달방식이 효율적이다.
class Point
{
public:
Point(int x, int y);
// ...
void setX(int newVal);
void setY(int newVal);
};

struct RectData
{
Point ulhc;
Point hlhc;
};

class Rectangle
{
private:
shared_ptr<RectData> pData;
public:
Point& upperLeft() const { return pData->ulhc;}
Point& lowerRight() const { return pData->hlhc;}
};

// Point 반환된 참조자 객체를 통해서 Point가 수정될수 있다. 즉 캡슐화에 문제가 생긴다.

class Rectangle
{
const Point& upperLeft() const { return pData->ulhc;}
const Point& lowerRight() const { return pData->hlhc;}
}
// 하나는 해결되었지만, Dangling Handle 문제가 있을수 있다.

Item 29 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자

Exception safety을 확보하는 작업은 매우 힘들다.

예외 안정성을 확보하기 위한 조건

  1. 자원이 새도록 만들지 않는다.
  2. 자료구조가 더럽혀지는 것을 허용하지 않는다.

예외 안정성을 갖춘 함수가 보장하는 것

  1. 기본적 보장
    1. 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지.
  2. 강력한 보장
    1. 함수 동작 중에 예외가 발생하면, 프로그램 상태를 절대로 변경하지 않겠다는 보장. Atomic 동작이라고 할수 있음.
  3. 예외 금지 보장
    1. 절대 예외를 던지지 않겠다는 보장. 약속한 동작은 언제나 끝까지 완수하는 함수

정리

  1. 강력한 예외 안정성 보장은 복사 후 Swap 방식을 써서 구현할수는 있지만, 모든 함수에 대해 실용적이지 않는다.

Item 30 인라인 함수는 미주알고주알 따져서 이해해 주자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 문맥별(constext-spectific) 최적화를 걸기 용이해 진다. 실제로 대부분 컴파일러는 아웃라인 함수 호출에 대해서 이런 최적화를 적용하지 않는다.

인라인 함수는 함수 호출문을 그 함수의 본문으로 바꿔치기 하는 것. 메모리가 제한된 컴퓨터에서 인라인을 남발하면, 가상 메모리 환경이라고 할지라도 성능의 걸림돌이 된다. 페이징 횟수가 늘어나고, 캐시 적중률도 떨어질 가능성이 있다.

본문 길이가 굉장히 짧은 인라인 함수를 사용하면, 코드의 크기도 작아지고 명령어 캐시 적중률도 증가한다.

inline은 컴파일러에게 요청을 할뿐, 명령이 아니다.

인라인 함수와 템플릿은 대체적으로 header에 있어야 한다. 왜나하면 대부분 빌드 환경에서 인라인을 컴파일 도중에 수행하기 때문이다.

  1. 함수 인라인은 작고, 자주 호출된느 함수에 대해서만 사용한다. 이렇게 하면, 디버깅 및 라이브러리의 바이너리 업그레이드가 용이하고, 코드 부풀림 현상이 최소화되고 속력이 빨리진다.
  2. 함수 템플릿이 대개 헤더 파일에 들어간다는 일반적인 부분만 생각해서 이들을 inline으로 선언하면 안된다.

Item 31 파일 사이의 컴파일 의존성을 최대로 줄이자

C++가 인터페이스와 구현을 깔끔하게 분리하는 일은 별로 좋지 않다.

C++의 클래스 정의는 클래스 인터페이스만 지정하는 것이 아니라 구현 세부사항까지 지정하고 있음.

각 객체의 의존성이 파일과 헤더 파일 사이의 컴파일 의존성을 야기하게된다. 또한 이경우 빌드 시간이 늘어난다.

해당 클래스에 대해 전방선언을 하고, 필요하다면 소스파일에서 #include 한다.

전략

  1. 객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않는다.
    • 어떤 타입에 대한 참조자 및 포인터를 정의할때는 그타입의 선언부만 필요함. 반면 어떤 타입의 객체를 정의할때에는 그 타입의 정의가 준비되어야 함
  2. 할수 있다면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만든다.
    • 어떤 클래스를 사용하는 함수를 선언할때에는 그 클래스의 정의를 가져오지 않아도 된다. 그 클래스 객체를 값으로 전달하거나 반환하더라도 클래스 정의가 필요없다.

정리

  1. 컴파일 의존성을 최소화하는 작업의 배경이 되는 가장 기본적 아이디어는 정의대신에 선언에 의존하게 만들자는 것이다. 두가지 접근 방법은 핸들 클래스와 인터페이스 클래스
  2. 라이브러리 헤더는 그 자체로 모든 것을 갖추어야 하며, 선언부만 갖고 있는 형태여야 함.