본문 바로가기
프로그래밍/C++

More Effective C++ 요약정리

by 건우아빠유리남편 2009. 11. 2.
반응형


http://synch3d.com/wiki/moin/moin.cgi/MoreEffectiveC_2b_2b#line10
  1. More Effective C++ 요약
    1. 기본 개념들
      1. 항목 1. 포인터(pointer)와 참조자(reference)를 구분하자.
      2. 항목 2. 가능한 C++ 스타일의 캐스트를 즐겨 쓰자.
      3. 항목 3. 배열과 다형성은 같은 수준으로 놓고 볼 것이 아니다.
      4. 항목 4. 쓸데 없는 기본 생성자는 그냥 두지 말자.
    2. 연산자(Operator)
      1. 항목 5. 사용자 정의 타입변환 함수에 대한 주의를 놓지 말자
      2. 항목 6. 증가 및 감소 연산자의 전위(prefix)/후의(postfix) 형태를 반드시 구분하자.
      3. 항목 7. &&, ||, 혹은 , 연산자는 오버로딩 대상이 절대로 아니다.
      4. 항목 8. new와 delete의 의미를 정확히 구분하고 이해하자
    3. 예외 (Exceptions)
      1. 항목 9. 리소스 누수를 피하는 방법의 정공(正攻)은 소멸자이다.
      2. 항목 10. 생성자에서는 리소스 누수가 일어나지 않게 하자.
      3. 항목 11. 소멸자에서는 예외가 탈출하지 못하게 하자.
      4. 항목 12. 예외 발생이 매개변수 전달 혹은 가상 함수 호출과 어떻게 다른지를 이해하자.
      5. 항목 13. 발생한 예외는 참조자로 받아내자
      6. 항목 14. 예외 지정(exception specification) 기능은 냉철하게 사용하자.
      7. 항목 15. 예외 처리에 드는 비용에 대해 정확히 파악하자.
    4. 효율(Efficiency)
      1. 항목 16. 뼛속까지 잊지 말자, 80-20 법칙!
      2. 항목 17. 효율 향상에 있어 지연 평가(lazy evaluation)는 충분히 고려해 볼 만한다.
      3. 항목 18. 예상되는 계산 결과를 미리 준비하면 처리비용을 깎을 수 있다.
      4. 항목 19. 임시 객체의 원류(原流)를 정확히 이해하자.
      5. 항목 20. 반환값 최적화(return value optimization)가 가능하게 하자.
      6. 항목 21. 오버로딩은 불필요한 암시적 타입변환을 막는 한 방법이다.
      7. 항목 22. 단독 연산자(op) 대신에 =이 붙은 연산자(op=)를 사용하는 것이 좋을 때가 있다.
      8. 항목 23. 정 안 되면 다른 라이브러리를 사용하자!
      9. 항목 24. 가상 함수, 다중 상속, 가상 기본 클래스, RTTI에 들어가는 비용을 제대로 파악하자.
    5. 유용하고 재미있는 프로그래밍 기법들(Techniques)
      1. 항목 25. 생성자 함수와 비(非)멤버 함수를 가상 함수처럼 만드는 방법
      2. 항목 26. 클래스 인스턴스의 개수를 의도대로 제한하는 방법
      3. 항목 27. 힙(heap)에만 생성되거나 힙에만 만들어지지 않는 특수한 클래스를 만드는 방법
      4. 항목 28. 스마트 포인터(Smart pointer)
      5. 항목 29. 참조 카운팅(Reference Counting)

More Effective C++ 요약

이런책을 원서로 보면 좋을듯 한데..
시간상의 이유와 귀차-_-니즘의 발동으로...
번역자분이 Effective C++과 달라서 약간의 느낌이 다름.
번역은 반역이다!!

기본 개념들

항목 1. 포인터(pointer)와 참조자(reference)를 구분하자.

가장 쉽게 볼수 있는 차이점은 연산자의 차이다.
포인터의 경우 *과 ->을 쓰고 참조는 .을 쓴다.
개념상의 차이점은 포인터의 경우 null을 가르킬수 있고 참조의 경우 null을 가르키는 것은 참조라 볼수 없다.
이 때문에 참조는 개념상 null을 검사할 필요가 없어 유용하다.
C 언어에는 참조의 개념은 생략되었는데 C++에 들어오면서 클래스가 생기면서 명확한 개념으로 자리 잡힌듯 하다.

항목 2. 가능한 C++ 스타일의 캐스트를 즐겨 쓰자.

단순한 C 스타일의 캐스트인 괄호()는 너무나도 범용적인 의미를 지녀서 해석상 오류가 생기기 쉽다.
더욱이 C++로 넘어오면서 클래스와 상속으로 인해 의미를 식별하기 힘든 경우도 생길수 있다.

그래서 C++에서는 static_cast, const_cast, dynamic_cast, reinterpret_cast 이 네가지 연산자가 추가 되었다.

static_cast는 C 스타일의 캐스트와 동일한 의미와 형변환 능력을 가지고 있다.
long형을 int형으로 우겨 넣는다던지 double형을 float형으로 넣을때 사용하면 된다.

const_cast는 상수성이나 휘발성(volatile)을 제거하는데 쓰인다.
const int를 int형으로 바꾸는 경우나 volatile int를 int형으로 바꾸는 경우에 쓰인다.

dynamic_cast는 상속의 경우에서 가상함수가 사용되는 경우 파생(derived)클래스나 형제(sibling)클래스로 변환할때 쓰인다.

class Widget { ... };
class SpecialWidget? : public Widget { ... };

...

SpecialWidget? *psw;
Widget pw;

psw = dynamic_cast<SpecialWidget? *>(&pw);

reinterpret_cast는 static_cast연산자가 처리 못하는 대부분을 형변환 시킬수 있다.
그러나 이 연산자는 컴파일러마다 다르게 정의되므로 이식성에 문제가 있다.
struct형을 long *형으로 바꿀수도 있고 float형을 bool형으로 바꾸는등 자유자재로 사용한다.

위의 연산자들은 변환이 불가능할때는 예외를 발생한다.

항목 3. 배열과 다형성은 같은 수준으로 놓고 볼 것이 아니다.

배열은 연속된 메모리상의 주소를 가르키기 때문에 지정된 범위내에서 상수시간으로 접근이 가능하다.
만약 배열이 클래스일때 배열상의 다음위치는 (배열시작위치 + 클래스의크기 * 다음인덱스)으로 생각할 수 있다.
그렇지만 형변환을 통해 파생클래스가 배열의 값으로 들어온다면 파생클래스내의 멤버들의 크기로 인해 다음 위치에 대한 접근이 불가능해진다.

항목 4. 쓸데 없는 기본 생성자는 그냥 두지 말자.

기본 생성자는 클래스 정의시 생성자가 생략되었을 경우에 자동으로 만들어 준다.
그러나 기본 생성자에는 약점이 많다. 객체의 멤버에 대한 초기화에 대한 보장이 없기 때문이다.
하지만 기본 생성자가 없으면 배열의 선언이 어려워 진다.
물론 피해가는 방법을 찾으면 없지는 않으나 복잡하고 구현상의 어려움이 따른다.
그렇다고 기본 생성자를 없이 만들게 되면 가상 기본 클래스를 만들때 어려움이 따른다.
이는 구현하는 쪽에서 미리 기본 클래스의 멤버를 초기화 해줘야 하는데 이것은 어떤 상속시키는 의미를 희석 시키기 때문이다.

결론은 차라리 기본 생성자를 생기도록 나두지 말고 그냥 생성자를 만들어 쓰자.

  • 어느정도 모호한 부분이 있음. 뒷 부분 읽고 다시 돌아 올것

연산자(Operator)

항목 5. 사용자 정의 타입변환 함수에 대한 주의를 놓지 말자

기본적인 C언어 타입의 암시적 타입 변환에 대해서는 변경할수 없다.(hard-coding 되어 있음)
그러나 C++의 확장에 의한 타입의 변환은 사용자가 명확하게 정의할 수 있다.

컴파일러가 정의한 타입변환은 두가지 종류가 있다.
하나는 단일 인자 생성자(single-argument constructor)이고

class Name
{
    Name(const string &s); // string 타입을 Name 타입으로 바꿉니다.

    ...

};

다른 하나는 암시적 타입변환 연산자(implicit type conversion)이다.

class Rational
{
    public:

      ...

      operator double() const; // Rational 형을 double 형으로 암시적으로 바꿉니다.

};
이 방법은 분명 편할수도 있으나 만약의 경우 모호하게 넘어가버리는 경우가 생겨 버그의 원인이 될수도 있다.
C++표준의 string형에는 char *형으로 암시적인 타입변환을 허용하지 않고 멤버인 c_str()을 두어 변환을 하게 한다.
암시적인 타입변환은 그다지 추천하지 않는데 최근 C++에 추가된 내용중 explicit 키워드를 사용하여 암시적 타입변환을 막을수 있다.
(그러나 명시적 타입변환은 여전히 허용하고 있다.)

그렇다면 또 다른 방법은 프록시 클래스(proxy class)를 사용하면 된다.
C++의 규칙에 두번의 암시적 타입변환은 이루어 질수가 없다는 구문이 있기 때문에.
단일 인자 생성자의 경우 이너 클래스(inner-class)를 두어 프록시로 사용하면 간단하게 해결된다.

항목 6. 증가 및 감소 연산자의 전위(prefix)/후의(postfix) 형태를 반드시 구분하자.

구현 되어 있는 형태가 다르고 쓰임이 다르다.

class UPInt
{
    public:
      UPInt& operator++(); // 전위 ++
      const UPInt operator++(int); // 후위 ++ int값은 무!조!건! 0임

      UPInt& operator--(); // 전위 --
      const UPInt operator--(int); // 후위 -- int값은 무!조!건! 0임

    ...

};

내부적 구현은 밑과 같다. C++ 공식적인 구현 방법

UPInt& UPInt::operator++() // 전위
{
    (*this) += 1;
    return *this;
}

const UPInt UPInt::operator(int)
{

    const UPInt oldValue = *this;
    ++(*this);

    return oldValue;

}

효율을 위해서라면 전위 연산자의 사용을 권한다.

항목 7. &&, ||, 혹은 , 연산자는 오버로딩 대상이 절대로 아니다.

&&과 ||은 단축평가 의미구조(short-circuit semantics)의 형태이다.
그러나 오버로딩을 하게되면 이것은 함수호출의 형태로 바뀌게 된다.

멤버 함수일 경우

if(expression1.operator&&(expression2))
    ...
전역 함수일 경우
if(operator&&(expression1,expression2))
    ...

이렇게 바뀌게 되는데 이는 expression1과 expression2가 이미 평가를 마친상태에서 이루어지기 때문에 단축평가가 불가능해진다.

또한 쉼표(,) 연산자의 경우도 마찬가지의 이유로 먼저 평가되고 그 다음 순차적으로 이루어져야 하는 반면 오버로딩을 하게되면
왼쪽부터 순차적으로 이루어진다는 보장이 없다. (컴파일러가 지원하는지 조차 모른다-_-;)

오버로딩 불가능한 연산자.
. .* :: ?: new delete sizeof typeid static_cast dynamic_cast const_cast reinterpret_cast

오버로딩 가능한 연산자.
operator new
operator delete
operator new[]
operator delete[]
+ - * / % ^ & | ~ ! = < > += *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- , ->* -> () []

항목 8. new와 delete의 의미를 정확히 구분하고 이해하자

new연산자는 오버로딩을 할수 없다.
하지만 operator new는 오버로딩이 가능하다.

일반적으로 new연산자를 사용하면 다음과 같은 코드가 생성된다.

string *ps = new string("Memory Management");

아래와 같이 변한다.

void *memory = operator new(sizeof(string)); // 미초기화된 메모리를 반환한다.
string::string("Memory Management"); // 생성자로 초기화한다.
string *ps = static_cast<string*>(memory); // 캐스트

여기서 operator new가 쓰이는데 이 연산자는 오버로딩이 가능하다.
따라서 사용자가 지정한 메모리 영역을 설정하여 그 부분을 할당하여 줄수 있는데
이를 메모리 지정 new(Placement new)라고 부른다.

사용방법은 다음과 같다.

new (사용자 지정 메모리)할당할 객체(객체의 크기);

그리고 operator new는 다음과 같이 오버로딩한다.

void *operator new(size_t, void *location)
{
    return location;
}

new와 마찬가지로 delete 연산자도 역시 유사한 코드를 생성한다.

string *ps;

...

delete ps;

아래와 같이 변한다.

ps->~string(); // 객체의 소멸자가 호출된다.
operator delete(ps); // 메모리를 해제한다.

이와 같이 operator new와 operator delete는 C언어의 malloc과 free와 유사하다.
단지 메모리를 생성해줄뿐 내부적인 생성자의 호출은 new 연산자에 존재한다.

또한 배열의 경우도 마찬가지이다.
단지 operator new ] 와 operator delete [
?가 호출된다는 점만 다르다.

예외 (Exceptions)

항목 9. 리소스 누수를 피하는 방법의 정공(正攻)은 소멸자이다.

동적 할당 리소스는 객체로 포장하라!

객체가 아닌 포인터로 조작하고 있을때 예외가 발생한다면 리소스 누구가 발생한다.
이럴때는 스마트 포인터(smart pointer)와 같은 방법으로 객체화 시켜준다.

C++표준 라이브러리중 auto_ptr 클래스가 있다.

template <class T>
class auto_ptr
{
    public:
      auto_ptr(T *p = 0) : ptr(0) {}
      ~auto_ptr() { delete ptr; }
    private:
      T *ptr;

};
대략적인 구조는 위와 같은데 이것은 지역객체로 존재하면서 포인터를 받아 예외발생시 확실한 리소스 해제를 보장하여 준다.

항목 10. 생성자에서는 리소스 누수가 일어나지 않게 하자.

만약 생성자에서 예외가 발생한다고 생각해보자.
C++에서는 생성 과정이 완료된(full constructed) 객체만을 안전하게 소멸시키기 때문에 생성자에 예외가 일어난 객체의 정상적인
소멸을 보장 받을수 없다.

생성자에서 try - catch문으로 예외구문을 처리하여도 되지만 앞의 항목의 auto_ptr을 사용하는것도 좋은 방법이다.

항목 11. 소멸자에서는 예외가 탈출하지 못하게 하자.

절대로 소멸자에서 예외가 발생시 절대 탈출하지 못하도록 막아야한다.
만약 이미 예외 발생으로 소멸자가 호출시 다시 소멸자 내부의 코드로 예외가 발생한다면 terminate 되어 실행이 바로 종료되어 버린다.
그리고 소멸자에서 예외가 발생이 전파되어진다면 역시나 그 객체의 정상적인 종료가 되지 못한다.

...

Session::~Session()
{

    try
    {
      logDestruction(this);
    }
    catch(...) // 이와 같이 차라리 아무런 처리가 없더라도 절대 소멸자의 예외가 외부로 전파되는것은 막아야 한다.
    {
    }
}

항목 12. 예외 발생이 매개변수 전달 혹은 가상 함수 호출과 어떻게 다른지를 이해하자.

함수로의 매개변수 전달과 catch문으로의 예외 전달의 가장 큰 차이점은
예외의 경우에는 지역 변수의 전달시 영역의 벗어난 경우에도 보장을 해줘야 한다는 점이다.
다시 말하면 catch문으로 파라미터를 넘겨줄때
그 객체의 범위를 벗어난 인자가 전달될수도 있는데 자칫 잘못하면 지역객체의 경우 스텍 되감기를 통하여 사라져 버릴수도 있다.
그렇기 때문에 예외의 파라미터의 경우에는 반드시 원본의 복사물의 형태로 전달되어야 한다.

예외 발생의 매개변수 전달의 경우를 알아보자면

...

catch(Widget w)

...

이와 같은 경우 전달되는 객체에 대하여 두 개의 사본이 만들어진다.
하나는 예외 복사 매커니즘에 의해 생기는 임시 객체이고 또 하나는 w로 저장될때 생기는 임시객체이다.

...

catch(Widget &w)

...

catch(const Widget &w)

...

위와 같은 경우는 한번의 복사만 이루어진다.
예외 복사 매커니즘의 한번의 복사와 그 객체애 대한 참조이다.

그리고 catch문 안의 throw는 예외를 전파(propagate)하고자 할때 쓰인다.

...

catch(Widget &w)
{

    ...

    throw;

}
이 경우에는 예외 자체를 그대로 전파하기 때문에 복사가 일어나지 않는다.

...

catch(Widget &w)
{

    ...

    throw w;

}
만약 이경우라면 예외 처리 매커니즘으로 다시 한번 사본과 함께 전파가 이루어진다.

이제 가상 함수의 호출 경우를 보기전에
예외의 파라미터는 타입변환을 허용하지 않는다는것을 알아두어야 한다.
만약 catch문에 double의 인자를 받아들이는 구문이 있을때 int형의 인자를 넘겨준다면 그 catch문은 실행되지 않는다.

그러나 상속 기반(inheritance-based)의 전달은 상위 호환성을 가진다.
예를 들면 표준 C++라이브러리의 예외 클래스의 경우 exception의 최상위 클래스부터 시작하여 logic_error과 runtime_error들로 나뉜다.
만약 catch문이 runtime_error를 받아들인다고 할때 exception의 예외를 전달한다면 runtime_error를 받아들이는 catch문에서
exception을 감지하고 예외를 처리하게 된다.

그리고 또 하나 알아둘점은 예외는 가장 첫째(first fit)를 선택하는 방식으로 진행되는데 상위 예외로 부터 하위 예외로 이어지는 코딩을
하지 않아야한다.

항목 13. 발생한 예외는 참조자로 받아내자

예외를 받아낼수 있는 방법은 세가지가 있다.
첫번째로 포인터로 받아내기인데 이것은 지역 객체의 전달이나 힙영역의 생성등의 문제로 쓰여지지 않는다.
두번째는 값을 이용한 전달인데 앞서 포인터의 전달과는 달리 거의 문제가 없어 보이지만 단하나 잘려지는 문제(slicing problem)가 생길수 있다.

밑은 잘려지는 문제의 예이다.

...

class Validation_error:public exception
{

    public:
      virtual const char *what() throw();

    ...
}

void someFunction()
{

    ...

    if(유효성 검사가 실패했을 경우)

      throw Validation_error();
}

void doSomething()
{

    try
    {
      someFunction();
    }
    catch(exception ex)
    {
      cerr << ex.what(); // exception.what()을 호출하고 Validation_error::what()은 호출되지 않는다.
    }
    ...
}

마지막으로 세번째는 참조를 이용한 경우인데 위의 슬라이싱 문제도 생기지 않고 가장 C++ 표준에 근접한 예외처리 방법이다.

항목 14. 예외 지정(exception specification) 기능은 냉철하게 사용하자.

예외 지정이란 함수가 발생시킬 예외를 미리 지정하는 방법이다.
미리 일어날 예외를 지정함으로 미리 일어날 예외를 예견해서 대처할수 있다는데 의미가 있다.
하지만 만약 지정된 예외가 아닌 다른 예외가 발생시 런타임 에러가 발생하면서 unexpected라는 특수 함수가 자동으로 호출된다.
unexpected의 기본 동작은 terminte를 호출하고 이 함수는 다시 abort를 호출하면서 프로그램은 지역객체조차 해제 되지 않고 바로 종료되어 버린다.

혹시라도 템플릿에 예외 지정을 사용하고자 한다면 포기하는게 좋다. 템플릿의 데이터 구조를 알수 없으므로 어떤 예외가 발생할지 예측하는건 너무 많은 요소가 존재하기 때문이다.
같은 이유로 콜백함수에 예외 지정을 사용하는것도 힘들지만 콜백함수 타입에 미리 예외 지정을 사용함으로 정상적인 동작을 할수 있다.

그렇더라도 뜻하지 않게 런타임 에러가 발생되는것은 참기 힘든일이다.
이것을 피할려면 unexpected가 호출하는 함수를 변경하여 주면 쉽게 해결가능하다.

void 예외 발생 함수()
{
    ... // throw를 사용하여 그냥 중개하는것도 하나의 방법이다.
}

...

set_unexpected(예외 발생 함수);

항목 15. 예외 처리에 드는 비용에 대해 정확히 파악하자.

코드가 약간(5~10%?) 생성된다고 한다. 그리고 예외 발생으로 함수가 복귀도는 시간은 일반적인 시간보다 1000배 가량 늘어난다고 한다.
하지만 중요한 점은 예외란 뜻하지 않는 상황이기 때문에 거의 일어나지 않는다고 생각할때 이 시간에 부담을 가질 필요는 없다.
만약 예외가 아닌 상황이라면 부담되는 시간은 없다고 봐도 되기 때문이다.

효율(Efficiency)

항목 16. 뼛속까지 잊지 말자, 80-20 법칙!

"프로그램 리소스의 80%는 전체 실행 코드의 약 20%만이 사용한다."
"실행 시간의 80%는 실행 코드의 약 20%만이 소모한다."

항목 17. 효율 향상에 있어 지연 평가(lazy evaluation)는 충분히 고려해 볼 만한다.

당장 필요해지기 전까지는 최대한 실행 시간을 늦춘다.

몇가지 요소로 알아보면
참조 카운팅(Reference Counting) 만약 문자열 두개를 합친다면 복사가 이루어지게 된다.
하지만 최대한 늦춰서 우선 문자열 두가지를 포인터 등으로 이어두기만 하면 우선 당장은 두 문자열이 합쳐진것과 같은 효과를 지닌다.
만약 두 문자열중 하나가 수정이 가해야 한다면 그제서야 복사를 해도 충분하다.

데이터 읽기와 쓰기를 구분하기 나중!!!!!!!!!!!!! 항목30을 참조

지연 방식의 데이터 가져오기(lazy fetching) 만약 데이터베이스에서 필드를 읽어온다고 가정하면 두번째 필드가 우선 필요할경우 두번째 필드만 데이터베이스로부터 읽어온다.
차후 나중에 필요할대 다른 필드도 읽어오는 방식으로 최소한의 시간으로 바로 다음 명령을 처리할수 있다.

  • mutable은 멤버함수내 어느곳에서도 (const 멤버 함수에서도) 수정이 가능하다. 만약 멤버외 인자로 넘어온 파라미터만 수정이 불가능하다면

해당 멤버 변수들에 mutable를 써넣으면 쉽게 구현가능하다.

지연 방식의 표현식 평가(lazy expression evaluation) 앞의 이야기와 비슷한데 만약 데이터 계산된 부분만 필요할 경우 부분 부분만 필요할때 계산하는게 유리하다.

항목 18. 예상되는 계산 결과를 미리 준비하면 처리비용을 깎을 수 있다.

위와는 조금 다른 시선으로 보면 만약 중복되어 많이 사용되는 부분이라면 위와는 반대로 오히려 미리 계산 해두는게 캐시와 같은 효과를 받아 총 실행시간을 단축 시킬수 있다.

항목 19. 임시 객체의 원류(原流)를 정확히 이해하자.

임시 객체는 뜻하지 않은 여러곳에서 쓰일수도 있다.

size_t countChar(const string &str, char ch);

...

char bufferMAX_STRING_LEN?;
char c;

...

countChar(buffer, c);

이것은 정상적으로 실행이 된다.
buffer은 char형 배열이지만 string형으로 암시적으로 바뀌어 임시객체가 생성된후 거기에 대한 레퍼런스가 넘어간다.
만약 인자의 타입이 const string &str이 아니라 string &str이었다면 실행이 불가능했겠지만 위와 같은 코드는 정상적으로 작동한다.

생각지 못한 임시객체 생성을 주의하자.

항목 20. 반환값 최적화(return value optimization)가 가능하게 하자.

반환값 최적화 RVO는 거의 대부분의 컴파일러가 지원하는 기능이다.
예상되어지는 임시객체를 생성을 억제할수 있는데 방법은
우선 함수는 인라인으로 두고 반환값을 생성자로 만들어주면 반환 받는 객체로 대체되어 진다.

inline const Rational operator*(const Rational &lhs, const Rational *rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator() );
}
이와 같이 만들어 두면 컴파일러가 반환객체를 함수에서 반환받는 객체로 대체하여 임시객체가 사라질수 있다.

항목 21. 오버로딩은 불필요한 암시적 타입변환을 막는 한 방법이다.

class UPInt
{
    public:
      UPInt();
      UpInt?(int value);

    ...
};

const UPInt operator+(const UPInt &lhs, consst UPInt &rhs);

UPInt upi1, upi2;

...

UPInt upi3 = upi1 + upi2; // 오버로딩된 +연산자가 정상 작동
UPInt upi4 = 10 + upi2 // 뜻하지 않게 암시적 타입변환을 통해 UPInt형으로 변환후 전달

이렇기 때문에 위의 예제에서 암시적 타입변환이 될수 있는 int형과 UPInt형을 예상해 최적화된 오버로딩을 추가하여주면 된다.

항목 22. 단독 연산자(op) 대신에 =이 붙은 연산자(op=)를 사용하는 것이 좋을 때가 있다.

연산자 오버로딩의 최적의 결과는 바로 임시객체가 없는 경우이다.

result = a + b + c; // a + b의 결과값이 임시객체 생성 다시 (a + b) + c에 임시객체 생성
result = a;
result += b;
result += c; // 전부 임시객체 생성 불필요
이와 같이 단독 연산자보다 최적화가 가능하다.

그리고 단독 연산자를

const Rational operator+(const Rational &lhs, const Rational &rhs)
{
    return Rational(lhs) += rhs;
}
이와 같이 =이 붙은 연산자와 연관 시켜주면 단독 연산자를 쉽게 구현할수 있다.

또한

const T operator+(const T& lhs, const T& rhs)
{
    return T(lhs) += rhs;
}
프렌즈 함수로 어렵게 구현할 내용이 깔끔하게 해결 된다.

항목 23. 정 안 되면 다른 라이브러리를 사용하자!

속도가 빠른 C라이브러리 약간은 느리지만 타입 안전성과 예외 처리도 가능한 C++라이브러리
모든 라이브러리 마다 설계자의 생각이 다르기 때문에 구현하기 힘들거나 시간이 많이 걸릴때
라이브러리를 찾아보는것 또한 좋은 방법이다.

항목 24. 가상 함수, 다중 상속, 가상 기본 클래스, RTTI에 들어가는 비용을 제대로 파악하자.

가상 함수는 가상 테이블(virtual table)과 가상 테이블 포인터(virtual table pointer)로 구현되어 있다.
가상 테이블은 객체 함수 포인터의 배열이나 리스트등으로 이루어 지는데 가상 함수나 상속 받은 클래스는 무조건 생성된다.
이 가상 테이블은 다중 상속시에도 가상 기본 클래스는 물론 RTTI에도 쓰인다.
RTTI의 경우 가상 테이블의 앞에 자신의 식별 코드가 있고 이 식별코드로 확인을 하고 실행을 한다.

유용하고 재미있는 프로그래밍 기법들(Techniques)

항목 25. 생성자 함수와 비(非)멤버 함수를 가상 함수처럼 만드는 방법

생성자는 가상함수로 만들수가 없다.
그러므로 상속받은 클래스에서 가상 함수를 호출하는 방식으로 가상 생성자(virtual constructor)로 만들수 있다.
가상 생성자중에서 가장 널리 쓰이는 예는 가상 복사 생성자(virtual copy constructor)이다.

class NLComponent
{
    public:
      NLComponent(const NLComponent &rhs);
      virtual NLComponent * clone() const = 0; // 복사 생성자에 이 함수가 사용되어진다.

    ...
}

class TextBlock? : public NLComponent
{

    public:
      virtual TextBlock? * clone() const // clone함수의 경우 가상 함수이기때문에 복사 생성자에서 clone의 호출은 각 객체에 맞는 함수가 실행된다.
      {
        return new TextBlock?(*this);
      }

    ...
}

이런 방법으로 비멤버 함수도 가상 함수처럼 동작하게 할수 있다.

class NLComponent
{
    ...

    public:

      virtual ostream& operator<<(ostream &str) const = 0;
}

class TextBlock? : public NLComponent
{

    ...

    public:

      virtual ostream& operator<<(ostream &str) const;
};

...

TextBlock? t;
t << cout; // 일관성및 가독성이 떨어진다.

class NLComponent
{
    ...

    public:

      virtual ostream& print(ostream &s) const = 0;
}

class TextBlock? : public NLComponent
{

    ...

    public:

      virtual ostream& print(ostream &s) const;
};

ostream &operator<<(ostream &s, const NLComponent&c)
{

    return c.print(s);
}
일관성및 가상함수 처럼 사용 가능하다.

항목 26. 클래스 인스턴스의 개수를 의도대로 제한하는 방법

객체를 생성이 이루어지는 세 가지 상황을 알아보면
첫번째는 그 자신이 객체로 만들어 질때이다. 이건 당연하다.
두번째는 상속 받을때 객체 생성이 이루어진다. 다시 말하면 부모클래스의 객체도 생성이 된다는 이야기다.
세번째는 다른 객체에 포함될때이다. 이것도 역시나 객체가 생성된다.

우선 객체를 하나만 생성해야 할때는 자신이 객체로 만들어질 경우 하나뿐이다.
(두번째와 세번째의 경우는 객체를 하나만 생성해야 되는 범위가 다른 클래스에게도 넘어간다.)
생성자를 private으로 하고 정적 객체를 friends나 static함수로 접근하면 쉽게 하나만 생성할수 있다.

이번에는 제한된 여러개의 객체를 생성할 경우 생성자를 private로 하고 유사 생성자(pseudo-constructor)로 만들면서 카운트 하면 된다.

class FSA
{
    public:
      static FSA *make FSA();
      static FSA *make FSA(const FSA& rhs);

    ...

    private:

      FSA();
      FSA(const FSA& rhs);
};

FSA *FSA::make FSA()
{

    return new FSA();
}

FSA *FSA::make FSA(const FSA &rhs)
{

    return new FSA(rhs);
}
이것은 유사 생성자의 예제이다. 호출한 쪽에서 delete를 해주어야 하는 부담이 있으므로 auto_ptr을 잘 활용하자.

하지만 이런 생성방법을 사용하면 코드의 중복된 부분이 많아질 것이라는 것을 쉽게 알수있다.
그렇기 때문에 카운트 부분을 템플릿과 상속을 활용하여 생성하면 된다.
카운트 부분의 클래스를 만들고 템플릿의 타입을 받는 부분에 만들고자 하는 클래스를 넣어서 private로 상속을 한다.
카운트 변수와 최대 만들고자 하는 변수들은 카운트 부분 클래스에 있으므로 using을 사용하요 private상속이지만 사용한다는 것을 표시하여주고
유사 생성자로 사용하면 카운팅 부분을 모을수 있다.

항목 27. 힙(heap)에만 생성되거나 힙에만 만들어지지 않는 특수한 클래스를 만드는 방법

힙영역에 생성을 하기 위해서는 반드시 new를 통하여 생성하여야 한다.
그렇기 위해서는 지역객체와 정적객체의 생성을 막아야 하는데 가장 쉬운 방법은 소멸자를 private나 protected로 만들어 버리고 유사 소멸자를 사용한다.

귀차나

항목 28. 스마트 포인터(Smart pointer)

포인터보다 스마트 포인터가 나은점.
생성(construction)과 소멸(destruction) 작업을 조절할 수 있다.
복사(copy)와 대입(assignment) 동작을 조절할 수 있다.
역참조(dereferencing) 동작을 조절할 수 있다.

template <class T> class SmartPtr?
{
    public:
      // 생성자와 복사 생성자와 소멸자
      SmartPtr?(T *realPtr = 0);
      SmartPtr?(const SmartPtr? &rhs);
      ~SmartPtr?();

      // 대입 연산자
      SmartPtr? &operator=(const SmartPtr? &rhs);

      // 역참조
      T *operator->() const;
      T &operator*() const;

      // 비교 연산자
      operator void*(); // bool연산자의 true와 대부분의 비교 연산자를 지원
      bool operator !() const; // bool 연산자의 false를 지원

      // 스마트 포인터 객체를 포인터로 변환
      operator T* ();

      // 상속의 경우
      template<class newType> operator SmartPtr?<newType>();

    private:
      T *pointee;
};

대입이나 복사 생성자의 경우 비트 단위 복사시 두개의 포인터가 생기며 스마트 포인터를 삭제시 두번 삭제되는 위험한 경우에 도달하게 된다.
그러므로 하나의 방법은 대입과 같은 연산시 소유권을 이전하여 준다. 대입하는 객체는 포인터를 NULL로 만들고 대입 받는 객체는 포인터가 전달된다. 


반응형

'프로그래밍 > C++' 카테고리의 다른 글

트리컨트롤(TreeCtrl) 사용법 종합  (0) 2009.11.08
MFC Tree Control  (0) 2009.11.06
Effective c++ 인터넷문서  (0) 2009.11.01
VC++ 자료형 외 표현범위  (0) 2009.10.30
야구하기_c++  (0) 2009.07.19

댓글