방프리

Effective STL 본문

C++/EffectiveSTL

Effective STL

방프리 2020. 1. 18. 03:59

항목 1. 적재적소에 알맞는 컨테이너를 사용하자.

 

- 각각의 컨테이너에는 장, 단점이 존재합니다. 프로그래머는 컨테이너들의 기능에 대해 잘 숙지하고 

만들고자 하는 기능에 맞게 컨테이너를 사용할 줄 알아야 합니다.

 

항목 2. "컨테이너에 독립적인 코드" 라는 환상을 조심하자.

 

- 컨테이너를 사용할 때 항상 하나의 컨테이너만 사용한다는 고정관념을 버려야 합니다. 유지보수나 제작 시에도 해당 기능의 방향이 달라질 수 있음을 고려해야 한다는 것입니다. 예를 들어 처음엔 vector 컨테이너가 적합하다고 생각하다가 나중엔 list 컨테이너로 바꾸어야할 상황이 올 수도 있기 때문입니다. 해결방법으로는 typedef를 활용하여 컨테이너를 선언하는 것입니다.

ex) typedef std::vector<int> VInt;

    VInt m_vInt;

 

항목 3. 복사는 컨테이너 안의 객체에 맞게 비용은 최소화하며, 동작은 정확하게 하자

 

- 컨테이너를 사용할 때 객체의 비용을 최소화 하면서 모든 동작을 제대로 하게끔 하는 방법은 객체의 포인터를 저장하는 것입니다. 모든 포인터의 비용은 4byte이므로 최소한의 비용으로 최대의 효율을 나타낼 수 있기 때문입니다. 더군다나 STL과 배열의 차이점은 배열의 경우 배열 최대의 사이즈만큼 생성자를 호출하지만 STL의 경우 사용자가 호출하지 않는 이상 생성자를 불러오지 않습니다.

 

항목 4. size()의 결과를 0과 비교할 생각이라면 차라리 empty()를 호출하자

 

- size() 함수의 경우 모든 요소를 검색해야 하기 때문에 절대로 상수 시간 동작을 수행할 수 없습니다. 하지만 empty() 함수의 경우 요소가 있는지 없는지만 판별하기 때문에 상수 시간 동작이 가능하죠 

즉 size() 함수의 경우 얼마나 시간이 걸릴지 예측이 불가능하지만 empty()의 경우 일정한 시간에 호출이 가능하다는 뜻입니다.

 

항목 5. 단일 요소를 단위로 동작하는 멤버 함수보다 요소의 범위를 단위로 동작하는 멤버 함수가 더 낫다. 

 

- 일일히 요소에 접근하여 함수를 실행하는 것보단 assign함수를 통해 한번에 처리하는 것이 더 효율적입니다. 

ex) for(int i =0; i < m_vInt.size(); i++) { std::cout << m_vInt[i] << std::endl; } -> 비효율적

    m_vInt.assign(m_vInt.begin(), m_vInt.end(), Print());  -> 효율적

 

항목 6. C++ 컴파일러의 어이없는 분석 결과를 조심하자

 

- 리스트 생성자를 사용하여 값을 대입할 때에는 생성자의 매개변수에 직접적으로 값을 넣는 것보단

값을 하나하나 선언하여 선언한 값을 넣어야 컴파일러가 헷갈리지 않는다는 뜻입니다. 

컨테이너의 범위를 던져주어서 하나하나 생성자를 호출하는 것이 컴파일러나 프로그래머나 서로에게 좋다는 뜻입니다.

 

항목 7. new로 생성한 포인터의 컨테이너를 사용할 때에는 컨테이너가 소멸되기 전에 포인터를 delete하는 일을 잊지 말자

 

- 아마 가장 많이 사용되지 않을까 생각됩니다. 일반 객체로 생성하면 모든 멤버변수의 양만큼 메모리를 잡아먹지만 포인터로 생성할 경우 포인터의 비용인 4byte만 사용되기 때문입니다. 그만큼 효율적이기 때문에 단점도 존재합니다. 바로 clear()로는 제대로 된 메모리 해제가 안된다는 점입니다. 컨테이너의 요소를 삭제하더라도 객체가 가지고 있는 메모리가 해제되는 것이 아니기 때문입니다. 포인터로 할당한 요소는 반드시 해제 후 요소를 삭제해야만 합니다.

 

항목 8. auto_ptr의 컨테이너는 절대로 만들지 말자

 

- COAP( Container of Auto Ptr)에 위반되는 방법입니다. C++규약 자체에서 auto_ptr을 통해 컨테이너 생성을 금지하고 있을 뿐더러 auto_ptr에서의 복사란 포인터의 값을 변경하는 행위이므로 여러 문제점이 발생하게 됩니다. 또한 COAP는 이식이 불가능하다는 큰 단점을 가지고 있습니다.

 

항목 9. 데이터를 삭제할 때에도 조심스럽게 선택할 것이 많다.

 

- 컨테이너에서 요소를 삭제할 때에도 고려해야 할 것이 많습니다. 크게 세 가지로 나뉘게 되는데요.

(1) 컨테이너에서 특정한 값을 가진 요소를 모두 없애려면 :

컨테이너가 표준 시퀸스 컨테이너면 erase-remove 합성문을 사용하고, list의 경우 list::remove를 사용합니다. 연관 컨테이너의 경우 erase 멤버 함수를 사용합니다.

(2) 컨테이너에서 특정한 조건문을 만족하는 객체를 모두 없애려면 :

컨테이너가 표준 시퀸스 컨테이너면 erase-remove-if 합성문을 사용합니다. list의 경우 list::remove-if를 사용하고 연관 컨테이너의 경우 remove_copy_if와 swap을 섞어 사용하던지, 컨테이너 내부를 도는 루프에서 erase를 호출하면서 erase에 넘기는 반복자를 후위 증가 연산자로 증가시킵니다.

(3) 루프 안에서 무엇인가를 하려면 (객체 삭제도 포함해서) :

시퀸스 컨테이너에서는 요소 하나하나 루프를 돌면서 erase를 호출할 때마다 그 함수의 반환값으로 반복자를 업데이트하는 일을 해야 합니다., 연관 컨테이너의 경우 똑같이 루프를 돌면서 erase를 호출하면서 erase에 넘기는 반복자를 후위 증가 연산자로 증가시킵니다.

위의 방식은 항목 2 독립적인 컨테이너를 사용하지 말자라는 규약에 어긋납니다. 하지만 컨테이너 별 erase의 반환형식이 다르므로 이 부분은 어쩔 수 없다고 판단됩니다.

 

항목 10. 할당자(allocator)의 일반적인 사항과 제약 사항에 대해 잘 알아두자

 

- 1. STL의 할당자는 C++의 new C의 malloc이 동작하는 방식과 다릅니다.

  2. STL 할당자는 typedef 타입을 제공합니다.

  3. STL의 할당자는 객체 그 자체를 사용합니다. 즉, 복사는 이루어지지 않습니다.

  4. 3번 항목의 이유 때문에 비정적(non-static) 데이터 멤버를 가지지 않습니다.

  5. opeartor new와 allocate는 다르게 동작하는데 operator new의 경우 바이트 개수에 맞추어 메모리를 할당하지만

     allocate는 객체의 개수에 맞추어 메모리를 할당합니다. (반환 타입 또한 전자는 void, 후자는 T*를 반환합니다.)

  6. STL의 모든 표준 연관 컨테이너는 자신이 생성될 때 같이 붙어온 할당자를 한 번도 호출하지 않습니다.

    

필자가 나열한 커스텀 할당자를 작성할 일이 있을 때 기억해야할 것들

1. 할당자를 템플릿으로 만듭니다. 템플릿 매개 변수에는 메모리를 할당하고자 하는 객체의 타입을 나타내는 T를 사용합니다.

2. pointer와 reference라는 typedef 타입을 제공하되, 항상 pointer는 T*, reference는 T&가 되도록 합니다.

3. 할당자는 비정적 데이터 멤버를 가질 수 없습니다.

4. allocate 멤버 함수는 객체의 개수를 매배 변수로 넘김니다.

5. 표준 컨테이너에서 필요로 하는 rebind라는 중첩템플릿을 꼭 제공합니다.

 

항목 11. 커스텀 할당자를 제대로 사용하는 방법을 이해하자

 

- 만약 여러분이 STL 할당자를 사용할 때 효율이나 속도, 단편화, 쓰레드 안정성 때문에 사용하지 않기를 원한다면 또한

새로운 STL할당자를 만들고 싶다면 이 항목의 방법을 사용하는 것을 추천드립니다.

공유 메모리에 하나 이상의 컨테이너를 만들어 여러 프로세스를 관리할 수 있도록 하는 방법을 통해 커스텀 할당자를 구현함으로써

위의 문제를 개발자는 해결할 수 있습니다. (이 항목에 대해서는 추가적으로 공부를 더 해야할 듯....)

 

항목 12. STL 컨테이너가 쓰레드 안정성에 대한 기대는 현실에 맞추어 가지자

 

- STL에서 다중쓰레딩 지원에 관련된 표준이라면은 다음의 내용을 권장합니다.

1. 여러 쓰레드에서 읽는 것은 안전합니다 : 하나 이상의 쓰레드가 하나의 컨테이너의 내용을 동시에 읽습니다, 다만 읽기 중엔 쓰기가 되지 않습니다.

2. 여러 쓰레드에서 다른 컨테이너에 쓰는 것은 안전합니다 : 하나 이상의 쓰레드가 다른 컨테이너에 동시에 쓸 수 있습니다. 

쓰레드가 컨테이너를 사용하는 것에 대한 문제에 대해서는 걱정할 것이 없으나 그 이외에 동시성 제어문제까지 STL이 관리해줄 수 없습니다.

 

항목 13. 동적으로 할당한 배열보다는 vector와 string이 낫다.

 

- C++에서는 new로 할당한 포인터는 항상 delete로 지워주어야 합니다. 하지만 STL에서는 자체적으로 메모리를 관리하기 때문에 신경 쓸 필요가 없습니다. 대신 string처럼 여러 군데에서 참조 카운팅 때문에 다중 쓰레드 환경에서 사용하기 불편한 것처럼 단점들 또한 존재합니다. 그럴 때엔 해당 기능의 사용을 막거나, 비슷한 라이브러리를 구현하거나, vector<char> 같은 다른 방법을 통해 사용하는 것으로 STL의 사용을 적극 추천합니다.

 

항목 14. reserve는 필요없이 메모리가 재할당 되는 것을 막아준다.

 

- vector와 string에만 해당되는 이야기 입니다. vector의 경우 요소가 추가되면 현재 있는 메모리의 절반을 늘려 메모리를 할당합니다. 요소에 딱 맞게 메모리가 할당된다면 참 좋지만 현실은 절대로 불가능하지요. 그래서 reserve를 통해 미리 필요한 메모리의 양만큼 할당을 하는 것입니다. reserve를 사용한다면 불필요한 메모리를 더 할당할 필요가 없지요. 만약 저장해야할 요소의 크기가 가늠이 되지 않는다면 한꺼번에 많은 양의 메모리를 할당한 후 데이터를 넣은 후에 남은 용량을 잘라내는 방법도 있습니다.

 

항목 15. 잊지말자! string은 여러 가지 방식으로 구현되어 있다는 사실을....

 

- string을 뜯어보면 여러 방식으로 구현되어 있는 것을 확인할 수 있습니다. 구현방식에 대해서는 책을 참고하시면 될 것 같습니다. string은 기본적으로 많은 라이브러리에서 참조 카운팅을 하지만 이 기능의 동작을 막는 방법도 제공합니다. string의 경우 포인터보다 일곱 배까지 커질 수 있습니다.

문자열을 새로 생성할 때 필요한 메모리 할당의 횟수는 0번, 1번 또는 2번이 될 수도 있습니다.

 

항목 16. 기존의 C API에 vector와 string을 넘기는 방법을 알아두자

 

- 기존의 STL을 C로 바꾸고 싶다면 컨테이너의 첫번째 요소 값의 주소와 크기를 넘겨주면 됩니다. 컨테이너도 배열과 마찬가지로 컨테이너의 주소는 요소의 첫 번째 주소값과 같기 때문입니다.

 

항목 17. 쓸데없이 남은 용량은 "바꿔치기(swap) 묘수"를 써서 없애 버리자

 

- vector에 메모리를 할당한 후 아무리 erase를 하더라도 vector가 차지하고 있는 capacity는 줄어들 지 않습니다. 그렇기에 우리는 새로운 컨테이너를 만들어 swap을 통해 바꿔 주어야만 합니다.

 

항목 18. vector<bool> 보기를 돌같이 하자

 

- STL 컨테이너의 자격을 갖추려면 "c가 타입 T의 객체를 담는 컨테이너라면 c에서 operator[]가 지원이 되어야 한다."가 지켜져야 합니다. vector<bool>은 애석하게도 이에 해당하지 않습니다. vector<bool>은 사용자가 보았을 때 실현되는 것처럼 보이나 실질적으론 정확히 동작하지 않습니다. 그러므로 bitset를 이용하거나 deque<bool>을 이용하는 것이 좋습니다. (deque<bool>은 정확히 동작합니다.)

 

항목 19. 상등 관계(equality)와 동등 관계(equivalence)의 차이를 파악하자

 

- 연관 컨테이너에서 find의 함수와 set의 insert 함수는 같은 요소가 있는지 찾는 함수로 알려져 있습니다. 하지만 이 두 함수는 요소를 찾는다는 공통점을 가지고 있지만 동작방식은 서로 다릅니다. find의 경우 상등성을 이용하여 찾지만 insert의 경우 동등성을 이용하여 요소를 찾습니다. 상등성은 operator==과 같은 기능을 하고 동등성은 operator<와 같은 기능을 한다는 것입니다. < 은 기본적으로 less를 사용하며 사용자 정의가 가능한 술어입니다. 그러므로 언제든지 정의가 가능하며 비교정리 또한 변환이 가능합니다. 

 

항목 20. 포인터를 저장하는 연관 컨테이너에 대해서는 적합한 비교(비교 함수자) 타입을 정해주자

 

- 연관 컨테이너에서 포인터로 객체를 담게 되면 데이터 비교시 처리가 불분명해집니다. 더욱이 포인터로 데이터를 담았기 때문에 출력 또한 불분명 해지죠 이것을 미연에 방지하기 위해 데이터를 비교하는 함수 어댑터나 포인터를 역참조 하는 함수를 선언해주어야 합니다.

ex) 함수 어댑터

struct StringPtrLess:

   public binary_function<const string*, const string*, bool> {

           bool operator() (const string *ps1, const string *ps2) const

           {

                   return *ps1 < *ps2;

            }

};

 

항목 21. 연관 컨테이너용 비교 함수는 같은 값에 대해 false를 반환해야 한다.

 

- set에서 insert 시 값과 똑같은 값을 가진 요소를 찾습니다. 항목 19번에서 나왔듯이 insert는 동등관계로 비교하기 때문에 항상 false를 반환해야 합니다. 만약 true를 반환하여 데이터가 들어가게 된다면 연관 컨테이너에서 가장 중요한 순서성이 망가지기 때문입니다.

 

항목 22. set과 multiset에 저장된 데이터 요소에 대해 키(key)를 바꾸는 일은 피하자

 

- map과 multimap에서 사용되는 키값은 const가 붙지만 set과 multiset의 키 값은 const가 붙지 않습니다. 그러므로 set과 multiset의 키 값은 수정이 가능하지만 수정을 진행할 시에 데이터가 깨지거나 소실될 확률이 커지므로 수정하지 않는 것이 좋습니다.

 

항목 23. 연관 컨테이너 대신 정렬된 vector를 쓰는 것이 좋을 때가 있다.

 

- vector는 모든 STL 동작 시간 중 가장 빠름니다. 연관 컨테이너들도 예외는 아니죠. 주로 연관 컨테이너를 사용하는 이유는 데이터를 찾는 시간이 로그 시간만큼 소요되기 때문에 많이 사용하는데 vector는 검색 시간 마저 줄어들게 해줄 수 있습니다. 바로 모든 데이터들이 sort가 되어 있다는 가정하에 말이죠. 연관 컨테이너는 앞, 자신, 뒤를 참조하는 포인터 3개를 가지고 노드를 생성하지만 vector는 연속적인 메모리를 사용하기 때문에 참조나 접근 등 여러 방면으로 빠릅니다. 이러한 장점을 가지고 데이터 검색에서도 vector를 사용하지만 앞에 나온 sort된 데이터 목록이어야 한다는 조건이 붙기 때문에 상황에 맞추어 써야합니다.

 

항목 24. map::operator[]나 map::insert는 효율 문제에 주의하여 선택하자

 

- map::operator[]는 데이터의 갱신처리를 할 때에 map::insert는 데이터의 추가를 진행할 때 그 빛을 빛냅니다. ma::operator[]를 데이터 추가 작업을 할 때에 사용한다면 쓸데 없는 함수를 불러오게 되고,

ma::insert를 데이터 수정작업에서 사용한다면 데이터가 있음에도 불구하고 데이터의 자료형의 생성자, 소멸자 등 굳이 불러올 필요가 없는 함수를 불러오게 됩니다. 가장 좋은 방법은 추가 및 갱신 시 데이터 존재 여부를 확인하여 insert 혹은 operator[]를 사용할 지 결정해주는 함수를 만드는 것이 가장 좋습니다.

 

항목 25. 현재는 표준이 아니지만, 해쉬 컨테이너에 대해 충분히 대비해 두자

 

- 책에서 필자는 해쉬컨테이너가 STL에 들어가지 않음을 애석하게 여기며 주로 사용되는 SGI 해쉬와 딩컴웨어에서 제공되는 해쉬 컨테이너로 예를 들어 설명했습니다. SGI 해쉬 테이블은 단일 연결리스트로 저장되고 딩컴웨어는 양방향 연결리스트 방식을 사용합니다. 각각 장, 단점을 가지고 있으며 사용자는 상황에 맞게 골라서 사용해야 합니다.

 

항목 26. const_iterator나 reserve_iterator, const _reverse_iterator도 좋지만 역시 쓸만한 것은 iterator이다.

 

- (1) iterator는 const_iterator와 reserve_iterator로 치환이 가능합니다.

  (2) reverse_iterator는 const_reverse_iterator와 base()를 통해 iterator로 치환이 가능합니다.

  (3) const_iterator는 const_reserve_iterator로 치환이 가능합니다.

  (4) const_reserve_iterator는 base()를 통해 const_iterator로 치환이 가능합니다.

중요한 점은 insert 와 erase 멤버 함수는 무조건 iterator를 넘겨야 합니다. 또한 const_iterator를 암묵적으로 iterator로 변환할 방법이 없으며 reserve_iterator를 iterator로 치환이 가능하나 일부분 수정이 필요합니다. 즉, iterator가 표준처럼 가장 널리 쓰이기 때문에 되도록이면 iterator를 사용하는 것을 추천합니다.

 

항목 27. const_iterator를 iterator로 바꾸는 데에는 distance와 advance를 사용하자

 

- STL에서 const_iterator를 iterator로 바꾸는 데에는 반드시 요소에 직접 접근하여 명시적으로 바꾸어 주어야 합니다. 암묵적 캐스팅이 통하지 않기 때문이죠. vector나 string의 경우 임의의 인덱스에 접근이 가능하기 때문에 비교적 쉽게 캐스팅이 되지만 list, map의 경우엔 상황이 다릅니다. 임의의 인덱스 접근이 어렵기 때문이죠. 그렇기 때문에 distance, advance 함수를 사용하여 임의의 요소에 접근한 후 명시적 캐스팅을 할 수 있도록 합니다. 

 

항목 28. reverse_iterator에 대응되는 기점 반복자(base iterator)를 사용하는 방법을 정확하게 이해하자.

 

- reverse_iterator와 iterator에서 요소에 접근하는 방식은 다릅니다. 왜냐하면 .end()가 가리키는 위치가 서로 다르기 때문이지요. iterator()의 .end()는 vector 요소의 맨 끝 요소의 다음을 가리키고 reverse_iterator의 .end()는 vector 요소의 맨 앞 요소의 앞을 가리키고 있기 때문입니다. 즉. reverse_itertor를 사용했을 경우와 iterator를 사용했을 경우 동일한 값으로 사용자가 원하는 결과를 얻을 수 없다는 뜻입니다. 

 

항목 29. 문자 단위의 입력에는 istreambuf_iterator의 사용도 적절하다.

 

- string 객체를 복사할 때 일반적으로 istream_iterator는 공백문자를 포함하지 않습니다. unsetf() 를 통해 공백문자도 포함시킬 수 있지만 istrea_iterator는 문자를 뽑는 것을 주로 하기 때문에 비효율적이지요. istreambuf_iterator는 문자열을 복사할 때 좋은 반복자이며 연산량을 단축하는데 일조합니다. 필자가 진행한 테스트의 경우 40%나 향상되었다고 합니다.

 

항목 30. 알고리즘의 데이터 기록 범위 (destination range)는 충분히 크게 잡자.

 

- vector나 string 같이 연속 컨테이너를 사용할 때 메모리를 미리 할당하고 사용하는 것이 효율적입니다. 그냥 사용할 시에 iterator 가 생성되지 않은 포인터를 가리킬 때도 있고 용량을 잡을 때 재할당이 일어날 수도 있기 때문입니다. 재할당은 연산량이 얼마 안되는 것 같지만 계속적으로 쌓이다보면 속도저하의 원인이 되기도 합니다. map, set 등 연관 컨테이너의 경우 미리 할당하면 오버헤드의 원인이 되기 때문에 연속 컨테이너를 사용한다면 미리 용량을 할당하시기 바랍니다.

 

항목 31. 정렬시의 선택 사항들을 제대로 파악해 놓자

 

- 요소들을 정렬할 때에 어떤 방식으로 정렬할 것인지에 따라 사용하는 알고리즘이 다릅니다. 책에서 소개된 알고리즘을 사용 용도에 따라 나열하면

1. sort & stable_sort(순서 유지 정렬) - 일반적인 정렬, 모든 요소를 순서대로 정렬할 시에 사용된다.

2. partial_sort - 부분 정렬, 상위 n개 즉, 일정 개수를 순서 상관없이 추출할 때 사용한다. 동등한 값이 있을 때에는 '내키는 대로'  동작한다.

3. nth_element - 상위 n개 즉, 일정 개수만큼 요소를 추출한 후 그 요소에 대해서만 정렬을 한다. 동등한 값이 있을 때에는 '내키는 대로' 동작한다.

4. partition & stable_partition - 표준 연속 컨테이너에서 요소들이 어떤 기준에 만족하는 것들과 만족하지 않은 것 들을 구분할 때사용한다.

5. 만약 list를 사용한다면 - list에서는 parition과 stable_partition을 직접 사용할 수 있으며 list는 자체적으로 list::sort 함수를 가지                               있다. 만약 partial_sort나 nth_element 기능이 필요하다면 

     (1) partial_sort or nth_element가 가능한 컨테이너로 복사한 후 사용 

     (2) list::iterator 컨테이너 하나 생성한 후 알고리즘을 적용하고 각 반복자를 통해 데이터를 엑세스

     (3) 반복자를 정렬해 놓은 컨테이너의 정보를 써서, 원하는 위치에 데이터를 splice 한다.

 

각 알고리즘의 연산량은 다음과 같습니다.

partition < stable_partition < nth_element < partial_sort < sort < stable_sort

 

항목 32. 요소를 정말로 제거하고자 한다면 remove 류의 알고리즘에는 꼭 erase를 붙여 사용하자

 

- 컨테이너의 요소를 삭제할 수 있는 것은 오로지 해당 컨테이너의 함수입니다. std::remove 함수의 경우 덮어쓰기 개념이지요.

첫 번째 요소부터 차근차근 넘어가면서 제거될 요소에 유지할 데이터를 대입합니다. 그렇기 때문에 비정상적으로 작동하죠. 그렇기 때문에 컨테이너 함수인 erase를 같이 병행해서 사용합니다. 다음 문장은 필수적으로 외워야 합니다.

v.erase(std::remove(v.begin(), v.end(), n), v.end());

list의 경우엔 remove 함수 자체 내에 remove와 erase를 병행해서 같이 사용하도록 정의되어 있습니다.

 

항목 33. remove와 비슷한 알고리즘을 포인터의 컨테이너에 적용할 때에는 각별히 조심하자

 

- 이 항목을 이해하려면 항목7의 내용과 remove가 어떻게 작동하는지를 이해해야 합니다. 항목7에 나온 내용에 따르면 컨테이너에 포인터로 만든 요소는 무조건 delete를 통해 메모리를 해제 후 삭제하도록 되어 있습니다. remove 함수를 사용할 때에도 예외는 아닙니다. remove함수의 동작은 앞서 나왔듯이 덮어 씌우기 개념이기 때문에 만약 delete를 하지 않고 사용한다면 메모리를 할당한 채로 그 메모리를 가리키는 포인터를 삭제해버리는 오류를 범하게 됩니다. 결국엔 메모리 누수 현상을 만들게 됩니다. 이런 오류를 미연에 방지하려면 partition 알고리즘을 사용하는 법도 좋습니다. 부득이하게 remove 함수를 사용해야 한다면, delete 하고 null을 가리키게 한 후에 삭제하는 방법도 좋은 예 중 하나입니다. 스마트 포인터를 사용할 경우 Reference Counting을 통해 삭제하기 때문에 remove-erase만으로도 충분합니다.

 

항목 34. 정렬된 범위에 대해 동작하는 알고리즘이 어떤 것들인지 파악해 두자

 

- binary_search, lower_bound, upper_bound, equal_range : 탐색 알고리즘은 정렬된 범위가 필요합니다. 보통 탐색 알고리즘은 로그 시간 안에 값을 찾는다고는 하지만 그 효율을 보장해주진 않습니다. 특히 임의접근방식을 사용하였을 때에만 이렇게 동작합니다. 양방향 접근자를 사용한다면 한 요소씩 건너 뛰어야 하기 때문에 탐색 알고리즘이라도 선형 시간 효율을 나타냅니다.

 

- set_union, set_intersection, set_difference, set_symmetric_difference : 집합 조작 알고리즘으로 선형 시간의 효율을 나타냅니다. 하지만 데이터가 정렬되어 있지 않는다면 이 알고리즘들은 선형 시간도 보장해주지 않습니다.

 

- merge, inplace_merge : 두 개의 정렬된 범위를 받아 합쳐 정렬된 또 다른 범위를 만드는 것입니다. 이 알고리즘은 선형 시간 안에 완료되지만 두 개의 범위가 모두 정렬된다는 전제 하에 동작합니다.

 

- includes : 어떠한 범위 안에 프로그래머가 원하는 값이 있는지 체크할 때 사용합니다. 이 알고리즘은 정렬되었다는 전제 하에 동작해서 선형 시간의 효율을 나타냅니다.

 

- unique, unique_copy : 범위 안의 중복된 요소를 삭제하고 단 하나의 데이터만 남길 때 사용합니다. remove와 비슷하게 동작하므로 정렬된 상태에서 더 빨리 동작합니다.

 

* 알고리즘을 사용할 시 어떤 방식으로 정렬되어 있는지 같이 넘겨주어야 합니다. ( 기본적으로 오름차순으로 정렬되었다는 가정하에 알고리즘들이 동작하기 때문에 미리 알려주어야 합니다.)

 

항목 35. 대소문자를 구분하지 않는 문자열 비교는 mismatch 아니면 lexicographical_compare를 써서 간단히 구현할 수 있다.

 

- strcmp와 비슷한 인터페이스로 구현한 함수

int ciCharCompare(char c1, char c2)

{

int lc1 = tolower(static_cast<unsigned char>(c1));

int lc2 = tolower(static_cast<unsigned char>(c2));

 

if(lc1 < lc2) return -1;

if(lc1 > lc2) return 1;

return 0;

}

 

int ciStringCompareImpl(const string& s1, const string& s2)

{

typedef pair<string::const_iterator, string::const_iterator> PSCI;

 

PSCI p = mismatch(s1.begin(), s1.end(), s2.begin(), not2(ptr_fun(chCharCompare)));

 

if(p.first == s1.end())

{

if(p.second == s2.end())

return 0;

else 

return -1;

}

 

return ciCharCompare(*p.first, *p.second);

}

 

- STL의 규정에 맞춘 술어 구문을 만들어, 연관 컨테이너의 비교 함수처럼 사용

 

bool ciCharLess(char c1, char c2)

{

return tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2));

}

 

bool ciStringCompare(const string& s1, const string& s2)

{

//lexicographical_compare : strcmp를 일반화한 알고리즘

return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);

}

 

항목 36. copy_if를 적절히 구현해 사용하자

 

- copy_if는 문제가 되는 객체의 요소를 제외한 나머지 요소들을 복사하는 것입니다. 실제로 STL 정식 함수에는 포함되어 있지 않아 개발자들이 스스로 구현을 해주어야 합니다. 다음의 예제는 필자의 말에 따르면 좋긴 하나 좀 더 보완이 필요한 copy_if를 구현한 함수입니다.

 

template<typename InputIterator, typename OutputIterator, typename Predicate>

OutputIterator copy_if(InputIterator begin, InputIterator end, OutputIterator destBegin, Predicate p)

{

while(begin != end)

{

if(p(*begin)) *destBegin++ = *begin;

++begin;

}

 

return destBegin;

}

 

항목 37. 범위 내의 데이터 값을 요약하거나 더하는 데에는 accumulate나 for_each를 사용하자

 

- 한 컨테이너에서 데이터 즉 하나의 객체를 정리할 때 accumulate 또는 for_each를 사용합니다. accumulate의 경우 자기 자신을 반환하지만 c++ 표준에 따라서 부가적인 효과에 영향을 받으면 안됩니다. for_each의 경우 자기 자신이 아닌 사본을 내어주기 때문에 부가적인 효과에 대해서는 제약이 없지만 필자의 경우엔 accumulate를 더 선호한다고 합니다. 어떠한 상황인지에 따라 조금씩 다르게 적용하면 될 것 같습니다.

 

항목 38. 함수자 클래스는 값으로 전달되도록(pass-by-value) 설계하자

 

- C와 C++에서 함수 포인터는 값으로 전달되는 것이 규칙이고 STL 함수 객체는 이 함수 포인터를 본뜬 것입니다. 그러므로 다음 아래의 규칙을 지켜야 합니다.

1. 함수 객체는 최대한 작게 만들어야 합니다.

2. 단형성으로 만들어야 합니다. 즉 가상함수가 존재하면 안됩니다.(슬라이스 문제)

 

하지만 이렇게 만들 경우엔 C++의 장점을 살리지 못하는(좋은 기능을 주는데도 사용하지 못하는) 경우가 발생합니다. 이러한 문제의 해결책은 함수자 클래스에 넣고 싶은 데이터나 가상 함수를 뽑아 이것을 다른 클래스로 옮깁니다. 즉 데이터의 양이 많은 클래스를 만든 후 그 클래스의 데이터를 멤버변수로 가지고 있는 작은 클래스를 하나 더 생성합니다. 이 작은 클래스를 함수자 클래스로 사용하고 이 클래스에서 데이터양이 많은 클래스의 데이터를 조작하도록 만드는 것입니다. Design Pattern책의 브릿지 패턴(Bridge Pattern)을 참고하면 될 것 같습니다.

 

항목 39. 술어 구문은 순수 함수로 만들자

 

- 시작 하기에 앞서 몇 가지 단어를 알아두고 가는 것이 좋을 것 같습니다.

술어 구문 : bool값을 반환하는 함수, true 또는 false만을 사용합니다.

순수 함수 : 함수가 반환하는 값이 그 함수의 매개 변수에 종속되는 함수를 일컫습니다.

술어구문 클래스 : operator()가 술어 구문인 함수자 클래스를 뜻합니다. 즉 이 함수가 true나 false를 반환합니다.

 

술어 구문 함수는 반드시 순수함수여야 합니다. 술어 구문 함수에서 변수가 변동이 있을 시 복사에 의해 함수가 어떻게 동작될지 아무도 장담하지 않기 때문이죠 operator() const를 통해 비슷하게 흉내낼 수는 있지만 순수 함수라는 해결책에 비하면 아무것도 아닙니다. 반드시!! 술어 구문은 순수함수로 만들어야 합니다.

 

항목 40. 함수자 클래스는 어댑터 적용이 가능하게(adaptable) 만들자

 

- 다른 포스트에서 설명

 

항목 41. ptr_fun, mem_fun, mem_fun_ref의 존재에는 분명한 이유가 있다.

 

- 다른 포스트에서 설명

 

항목 42. less<T>는 operator<의 의미임을 꼭 알아두자.

 

- std관련된 문서는 되도록 수정해서는 안됩니다. 프로그래머 사이에서 암묵적으로 알고 있는 것들(예로 operator+, operator-)은 그 것에 대한 기능만을 수행합니다. 즉, 다른 기능이 포함되서는 안된다는 뜻이죠 그러므로 less<T>의 비교형식에 무조건 operator<에 대한 의미만 부여하여 사용해야 합니다. 만약 다른 기능을 넣고 싶다면... 다른 함수 객체로 생성하여 사용하는 것이 좋습니다. 그렇게 하면 필자가 말하는 황당함 최소화의 원칙을 지키게 되죠 필자는 왜 하필 less<T>로 예를 들었을까요? 이유인 즉 STL의 컨테이너들은 기본 정렬구조가 less<T>를 가지고 있기 때문입니다.(less<T>를 기본으로 가지고 있기 때문에 추가 기능을 넣으려는 프로그래머들에 대한 경고입니다!!)

 

항목 43. 어설프게 손으로 작성한 루프보다는 알고리즘이 더 낫다.

 

- 반복문 보다 알고리즘이 더 좋은 이유는 효율성, 정확성, 유지보수성 이 세가지 이유가 있습니다.

1. 효율성 : 반복문은 매번 조건문에 비교해야 하기 때문에 마지막 인자를 검사하지만, 알고리즘에서는 검사하지 않습니다.

            각 컨테이너 별 루프가 도는 방식은 이미 최적화가 잘 된 함수가 있습니다.

2. 정확성 : 어떤 개발자가 작성한 루프보다 훨씬 명확한 의미를 전달합니다.

            일반 루프 구문보단 내용을 파악하기 쉽습니다.

 

항목 44. 같은 이름을 가진 것이 있다면 일반 알고리즘 함수보다 멤버 함수가 더 낫다.

 

- 컨테이너 문서를 보면 STL 알고리즘(algorithm에 있는 함수)와 비슷한 멤버함수가 있습니다. 이 때 둘 중 어느 것이 더 효율이 

좋을까요? 당연히 멤버 함수입니다. 그 이유는 컨테이너 별 아래와 같습니다.

1. 연관 컨테이너

(1) 일반 find 함수의 경우 선형시간이지만 멤버 함수 find는 로그 시간으로 실행합니다.

(2) 알고리즘 함수는 상등성을 기준으로 삼지만 멤버 함수는 동등성을 기준으로 삼습니다. 이 때문에 정확도가 높지 않습니다.

(3) map과 multimap에 적용하였을 때 각 컨테이너의 특징에 맞추어 멤버 함수는 동작하지만 알고리즘 함수를 그러지 못합니다.

 

2. list 컨테이너

(1) 알고리즘 함수는 객체를 복사하지만, 멤버 함수는 본래 객체를 조작하여 동작합니다.

(2) remove, erase 등 알고리즘 함수는 복사를 하여 삭제하지만, 멤버 함수는 직접 객체를 삭제합니다.

 

3. 그 이외에

(1) sort 함수는 list의 경우 양방향 알고리즘이기 때문에 사용할 수 없지만 멤버 함수는 list 컨테이너에 맞추어 동작합니다.

(2) merge 알고리즘은 알고리즘 함수의 경우 범위를 건들일 수 없지만 멤버 함수는 바꿀 수 있습니다.

 

항목 45. count, find, binary_search, lower_bound, upper_bound 그리고 equal_range를 제대로 파악해 두자

 

- 다른 포스트에서 설명

 

항목 46. 알고리즘의 매개 변수로는 함수 대신 함수 객체가 괜찮다.

 

- 다른 포스트에서 설명

 

항목 47. 쓰기 전용(write-only) 코드는 만들지 말자

 

- 코드를 작성할 때 자신이 사용하기 편한대로 작성해서는 안된다는 뜻입니다. 프로젝트를 진행할 때 보통 개인이 아닌 팀 단위로 프로젝트를 진행합니다. 또한 개발의 대부분이 유지보수라는 점을 감안한다면 코드를 작성시 무조건적으로 문장을 줄이는 것이 아닌 읽기 쉬운 코드로 작성해야 합니다. 분명 성능 위주로 작성하는 것도 좋지만, 항상 코드는 읽기 쉬워야 한다는 점을 간과해서는

안됩니다.

 

항목 48. 용도에 맞는 헤더를 항상 #include 하자

 

- 1. 거의 모든 컨테이너들은 같은 이름의 헤더에 정의되어 있습니다. ex) <vector>, <list>

  2. 네 개를 제외한 모든 알고리즘은 <algorithm>에 선언되어 있습니다. 나머지 accumulate, inner_product, adjacent_difference,

    partial_sum은 <numeric>에 선언되어 있습니다.

  3. 반복자는 모두 <iterator>에 있습니다.

  4. 표준 함수자(lest<T>)와 함수자 어댑터(not1, bind2nd)는 <functional>에 선언되어 있습니다.

 

항목 49. STL에 관련된 컴파일러 진단 메세지를 해석하는 능력을 가지자.

 

- 아래의 항목들은 필자가 책에서 소개하는 STL 컴파일러 메세지를 이해하는데 도움이 되는 힌트들입니다.

1. vector와 string의 경우 반복자는 포인터와 같습니다. 예를 들어, 소스 코드에 vector<double>::iterator가 있으면 컴파일러 메시지는 double* 입니다.

2. back_insert_iterator, front_insert_iterator, insert_iterator등을 운운하는 메시지는 거의 항상 back_inserter나 front_inserter inserter를 호출할 때 실수를 저질렀다는 뜻입니다.

3. 비슷한 식으로, 에러 메시지에서 binder1st나 binder2nd 등이 발견되었다면 bind1st 혹은 bind2nd를 잘못 호출했다고 생각하면 됩니다.

4. 출력 반복자(ostream_iterator, ostreambuf_iterator, back_inserter, front_inserter, inserter에서 반환된 반복자들)은 연산자(operator=) 내부에서 출력 혹은 삽입 동작을 취하기 때문에 대입 연산자 부분에서 에러가 발생합니다.

5. STL 알고리즘 내부 문제라고 에러 메시지가 나온다면 타입에 대해 문제가 있다는 뜻입니다.

6. vector나 string, for_each와 같이 자주 쓰이는 STL 컴포넌트를 사용하고 있는데 컴파일러 쪽에서 문제가 발생한다면 #include를 확인하세요 

 

항목 50. STL 관련 웹 사이트와 친구하자

 

- 1. SGI STL 사이트 http://www.sgi.com/tech/stl/.

  2. STLport 사이트 http://www.stlport.org/.

  3. Boost 사이트 http://www.boost.org/.

Comments