[C++] 스터디 CPPALOM 16주차: Effective C++ Item 46~53
파워 포인트 파일(.pptx)을 Markdown으로 변환하여 업로드하였음)
# <br>CPPALOM # <br>16주차 – Effective C++ item 46~53 한수빈 # <br>항목46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 * 템플릿 코드는 암시적 타입 변환을 고려해 정적 코드를 생성하지 않는다. * 다음 코드는 컴파일 에러를 일으킨다! * int형 2가 Rational 자료형으로 암시적 변환될 수 없기 때문이다. ```C++ template<typename T> class Rational { public: Rational(const T& numerator = 0, const T& denominator = 1); const T numerator() const; const T denominator() const; }; template<typename T> const Rational<T> operator*( const Rational<T>& lhs, const Rational<T>& rhs); Rational<int> oneHalf(1, 2); Rational<int> result = oneHalf * 2; ``` * 대신, 클래스 템플릿 안에 프렌드를 이용해서 선언하면, 프렌드 함수의 선언은 템플릿의 인자추론에 영향을 받지 않는다. * 선언 자체는 일반적인 함수가 되는 것과 마찬가지이다! * 하지만 이는 링크가 되지 않는다. * 클래스 자체는 선언되었으며, friend 멤버 함수는 non-template 함수로 컴파일된다. * 하지만 링커는 non-template 함수를 찾을 수 없다. template 함수만 존재하기 때문이다! ```C++ template<typename T> class Rational { public: friend const Rational operator*( const Rational& lhs, const Rational& rhs); }; template<typename T> const Rational<T> operator*( const Rational<T>& lhs, const Rational<T>& rhs) {} ``` * [https://isocpp.org/wiki/faq/templates#template-friends](https://isocpp.org/wiki/faq/templates#template-friends) * 따라서 두 가지 해결책이 있다. * 첫째, 함수 바디를 클래스 선언 안에서 정의한다. * 둘째, 클래스와 함수를 전방 선언 후 클래스 내 선언에 <>를 추가한다. ```C++ template<typename T> class Rational { public: friend const Rational operator*( const Rational& lhs, const Rational& rhs) { return Rational( lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } }; ``` ```C++ template<typename T> class Rational; template<typename T> const Rational operator*( const Rational& lhs, const Rational& rhs); template<typename T> class Rational { public: friend const Rational operator* <>( const Rational& lhs, const Rational& rhs) }; ``` # <br>항목47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 * iterator들을 일정 방향으로 이동시키는 advance() 함수가 존재한다. * 이는 효율을 위해 iterator의 종류에 따라서 각자 적절한 구현을 가져야 한다. * iterator 클래스의 특성 정보를 이용하면 되지만, 두 가지 문제가 존재한다. * 위 방식의 템플릿 함수가 양방향 반복자에 대해서 생성될 경우, 컴파일 에러가 난다. iter += d가 논리적으로 불가능하기 때문이다. (if문에 의해서 실행이 되지 않음에도) * 컴파일 시기가 아닌 런타임 시기에 행위가 결정된다. 이는 효율적이지 못하다. ```C++ template<typename IterT, typename DistT. void advance(IterT& iter, DistT d) { if (iter is random_access_iterator) iter += d; else if (d >= 0) while (d--) ++iter; else while (d++) --iter; } ``` 오버로딩을 이용해서 이를 영리하게 해결할 수 있다. ```C++ template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) { ... } template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) { ... } template<typename IterT, typename DistT> void advance(IterT& iter, DistT d) { doAdvance( iter, d, typename std::iterator_traits<IterT>::iterator_category() ); } ``` # <br>항목48: 템플릿 메타프로그래밍, 하지 않겠는가? * 템플릿 메타프로그래밍이란 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 것이다. * C++ 컴파일러 역시 규칙에 의해서 작동하는 튜링 머신이다. * 따라서, 템플릿을 통해 “정적인 코드 결과”를 생성하는 프로그램을 프로그래밍할 수 있다! * TMP는 컴파일 시기에 실행되는 프로그램이므로, 런타임에 오버헤드가 없다. * 가령, 사전에 계산되어야 하는 것을 컴파일 시기에 미리 계산시킬 수 있다. * 관심이 있다면 관련 서적을 찾아보자! # <br>항목49: new 처리자의 동작 원리를 제대로 이해하자 * new를 사용했을 때, 할당할 메모리가 부족하면 어떻게 동작할까? * 설정된 new_handler를 호출한다. * 이는 전역으로 설정된 것이다! * set_new_handler() 함수를 제공하며, 이를 통해서 void형 함수를 할당해 객체 생성에 실패했을 때 해당 함수가 호출되도록 할 수 있다. * 기존에 설정되어 있던 new_handler가 반환된다. ```C++ namespace std { typedef void (*new_handler)(); new_handler set_new_halnder(new_handler p) throw(); } ``` * 이러한 new 처리자는 다음 중 하나를 꼭 해야 한다. * 사용할 수 있는 메모리를 더 많이 확보한다. * 가령 사전에 메모리를 확보하였다가 new 처리자가 호출되면 해당 메모리를 소멸시키고 할당해준다. * 다른 new 처리자를 설치한다. * 현재의 new 처리자가 이를 해결할 수 없다면, 다른 new 처리자를 할당해 위임한다. * new 처리자의 설치를 제거한다. * new 처리자가 nullptr이라면, 예외를 던지게 된다. * 예외를 던진다. * bad_alloc 혹은 bad_alloc에서 파생된 타입의 예외를 던진다. * 복귀하지 않는다. * abort 혹은 exit를 호출한다. # <br>항목50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자 * new와 delet는 다음과 같은 목적을 위해 바꾼다. * 잘못된 힙 사용을 탐지하기 위해 * new를 호출할 때, 양쪽 경계에 int 하나씩을 추가로 할당한다. * 이 int는 고유한 바이트 패턴을 지닌다. e.g., 0xDEADBEEF * delete를 호출했을 때, 해당 바이트 패턴이 여전히 같은지 확인한다. * 만약 다르다면, 데이터 오버런이 발생한 것이다. * 효율을 향상시키기 위해 * 힙 단편화를 방지한다든가, 다양한 애플리케이션의 요구사항에 맞는 new 및 delete를 작성한다. * 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해 * 할당된 메모리 블록의 크기는 어떤 분포를 보이는지? * 각각의 사용 기간은 어떤 분포를 보이는지? * 메모리가 할당되는 순서가 FIFO인지 LIFO인지? * 시간 경과에 따라 사용 패턴이 바뀌는지? * 한 번에 쓰이는 동적 할당 메모리의 최대량은 얼마인지? * 할당 및 해제 속력을 높이기 위해 * 컴파일러가 제공하는 메모리 관리 루틴이 다중 스레드에 맞게 만들어져 있다면, 단일 스레드에 맞게 만들어 속력 이득을 보게 한다. * 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해 * 특정 자료구조들을 위치 지정 new 및 delet를 이용해 특정 위치에 몰려있게 한다. * 캐싱에서 이득을 볼 수 있다. * 그때그때 원하는 동작을 수행하도록 하기 위해 * 데이터의 보안 강화를 위해 해제한 메모리 블록에 0을 덮어쓰도록 한다. * 공유 메모리를 조작하는 일은 C API로밖에 할 수 없을 때 이를 new 및 delete를 변경하여 해결한다. # <br>항목51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 * operator new는 다음 요구사항을 지켜야 한다: * 반환 값이 제대로 되어 있어야 한다. * 가용 메모리가 부족할 경우에는 new 처리자 함수를 호출해야 한다. * 크기가 없는 메모리 요청에 대한 대비책을 갖춰야 한다. ```C++ void* operator new(std::size_t size) throw(Std::bad_alloc) { using namespace std; if(size == 0) size = 1; while (true) { // size byte를 할당 // if (할당이 성공했음) // return (할당된 메모리에 대한 포인터); // new_hanlder globalHandler = set_new_handler(nullptr); // set_new_handler(globalHandler); if(globalHandler) (*globalHandler)(); else throw std::bad_alloc(); } } ``` * 또한 상속의 문제를 고려해야 한다. * 파생 클래스가 new 연산자를 오버로딩하지 않을 경우 부모 클래스의 new가 호출되기 때문이다. * 즉, 자식 클래스임에도 불구하고 부모 클래스의 생성자가 호출되게 된다! * 따라서 다음과 같이 크기를 검사해야 한다. * delete의 경우도 유사하다. ```C++ void* Base::operator new(std::size_t size) throw(Std::bad_alloc) { if (size != sizeof(Base)) return ::operator new(size); } ``` ```C++ void Base::operator delete(void* rawMemory, std::size_t size) noexcept { if(rawMemory == nullptr) return; if (size != sizeof(Base)) { ::operator delete(rawMemory); return; } // rawMemory가 가리키는 메모리를 해제한다. return; } ``` # <br>항목52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자 * 위치지정 new 함수는 추가 매개변수를 받는 형태를 의미한다. * 생성자 호출 중 예외가 발생했을 때, C++ 런타임은 할당된 메모리를 소멸시킬 의무가 있다. * 이때 C++ 런타임은 호출된 new와 동일한 짝의 delete 호출한다. * 만약 짝이 없다면, 전역 delete를 호출한다. * 따라서 메모리 누출을 피하기 위해 new와 delete의 짝을 항상 맞춰주어야 한다! ```C++ void* oeperator new(std::size_t, void *pMemory) noexcept; ``` # <br>항목53: 컴파일러 경고를 지나치지 말자 * 컴파일러 경고를 쉽게 지나치지 말자. * 컴파일러에서 지원하는 최고 경고 수준에서도 경고 메시지를 내지 않고 컴파일되는 코드를 만드려고 노력하자. * 컴파일러 경고에 너무 기대지도 말자. * 컴파일러마다 경고가 천차만별이다. * 다른 컴파일러로 이식하면 수많은 경고가 생길 수도, 없어질 수도 있다.
댓글
댓글 쓰기