Effective STL 정리

효과적인 콘테이너 요리법



  • EffectiveSTL01 – 적재적소에 알맞은 컨테이너를 사용하자
  • EffectiveSTL02 – 컨테이너에 독립적인 코드라는 환상을 조심하자
  • EffectiveSTL03 – 복사(copy)는 컨테이너 안의 객체에 맞게 비용은 최소화하며, 동적은 정확하게 하자
  • EffectiveSTL04 – size()의 결과를 0과 비교할 생각이라면 차라리 empty를 호출하자
  • EffectiveSTL05 – 단일요소를 단위로 동작하는 멤버함수보다 요소의 범위를 단위로 동작하는 멤버함수가 더 낫다.
  • EffectiveSTL06 – C++ 컴파일러의 어이없는 분석결과를 조심하자
  • EffectiveSTL07 – new로 생성한 포인터의 컨테이너를 사용할 때에는 컨테이너가 소멸되기 전에 delete하는 일을 잊지 말자
  • EffectiveSTL08 – auto_ptr의 컨테이너는 절대로 만들지 말자.
  • EffectiveSTL09 – 데이타를 삭제할 때에도 조심스럽게 선택할것이 많다.
  • EffectiveSTL12 – STL 컨테이너가 쓰레드 안정성에 대한 기대는 현실에 맞추어 가자.

vector 와 string



  • EffectiveSTL13 – 동적으로 할당한 배열보다는 vector와 string이 낫다.
  • EffectiveSTL14 – reserve는 필요 없이 메모리가 재할당되는 것을 막아 주다.
  • EffectiveSTL15 – 잊지말자 string 은 여러 가지 방식으로 구현되어 있다는 사실을…
  • EffectiveSTL16 – 기존의 C API에 vector와 string을 넘기는 방법을 알아두자
  • EffectiveSTL17 – 쓸데없이 남은 용량은 “바꿔치기(swap)묘수”를 써서 업애 버리자
  • EffectiveSTL18 – vector<bool> 보기를 돌같이 하자

STL 연관 컨테이너



  • EffectiveSTL19 – 상등관계와 동등관계의 차이를 파악하자
  • EffectiveSTL20 – 포인터를 저장하는 연관컨테이너에 대해서는 적합한 비교(비교 함수자) 타입을 정해주자
  • EffectiveSTL21 – 연관컨테이너용 비교 함수는 같은 값에 대해 false를 반환해야 한다.
  • EffectiveSTL22 – set과 multiset에 저장된 데이터 요소에 대해 키(key)를 바꾸는 일은 피하자
  • EffectiveSTL23 – 연관 컨테이너 대신에 정렬된 vector를 쓰는 것이 좋을 때가 있다.
  • EffectiveSTL24 – map::operator[] 나 map::insert는 효율 문제에 주의하여 선택하자.
  • EffectiveSTL25 – 현재는 표준이 아니지만, 해쉬 컨테이너에 대해 충분히 대비해 두자.

반복자



  • EffectiveSTL26 – const_iterator 나 reverse_iterator, const_reverse_itertor도 좋지만 역시 쓸만한 것은 iterator 이다.
  • EffectiveSTL27 – const_iterator를 iterator로 바꾸는 데에는 distance와 advance를 사용하자.
  • EffectiveSTL28 – reverse_iterator에 대응되는 기점 반복자(base iterator)를 사용하는 방법을 정확하게 이해하자.
  • EffectiveSTL29 – 문자 단위의 입력에는 istreambuf_iterator의 사용도 적절하다.

알고리즘



  • EffectiveSTL30 – 알고리즘의 데이타 기록 범위는 충분히 크게 잡자.
  • EffectiveSTL31 – 정렬시의 선택 사항들을 제대로 파악해보자.
  • EffectiveSTL32 – 요소를 정말로 제거하고자 한다면 remove 류의 알고리즘에는 꼭 erase를 붙여 사용하자.
  • EffectiveSTL33 – remove와 비슷한 알고리즘을 포인터의 컨테이너에 적용할 때에는 각별히 조심하자.
  • EffectiveSTL34 – 정렬된 범위에 대해 동작하는 알고리즘이 어떤 것들인지 파악해 두자.
  • EffectiveSTL35 – 대소문자를 구분하지 않는 문자열 비교는 mismatch 아니면 lexicographical_compare를 써서 간단하게 구현할 수 있다.
  • EffectiveSTL36 – copy_if를 적절히 구현해 사용하자.
  • EffectiveSTL37 – 범위 내의 데이터 값을 요약하거나 더하는 데에는 accumulate 나 for_each를 사용하자.

함수자, 함수 객체, 함수, 기타 등등



  • EffectiveSTL38 – 함수자 클래스는 값으로 전달되도록 설계하자.
  • EffectiveSTL39 – 술어 구문은 순수 함수로 만들자.
  • EffectiveSTL40 – 함수자 클래스는 어댑터 적용이 가능하게(adaptable) 만들자.
  • EffectiveSTL41 – ptr_fun, mem_fun, mem_fun_ref 의 존재에는 분명한 이유가 있다.
  • EffectiveSTL42 – less<T>는 operator< 의 의미임을 꼭 알아두자.

STL 프로그래밍을 더 재미있게 해주는 팁 모음



  • EffectiveSTL43 – 어설프게 손으로 작성한 루프보다 알고리즘이 더 낫다.
  • EffectiveSTL44 – 같은 이름을 가진 것이 있다면 일반 알고리즘 함수보다 멤버 함수가 더 낫다.
  • EffectiveSTL45 – count, find, binary_search, lower_bound, upper_bound, 그리고 equal_range 를 제대로 파악해 두자.
  • EffectiveSTL46 – 알고리즘의 매개 변수로는 함수 대신 함수 객체가 괜찮다.
  • EffectiveSTL48 – 용도에 맞는 헤더를 항상 #include 하자
  • EffectiveSTL50 – STL 관련 웹 사이트와 친구하자

STL 관련 웹 사이트와 친구하자


SGI STL 사이트 #

[http]http://www.sgi.com/tech/stl/
STL 컴포넌트에 관해서 종합적으로 설명을 해놓았으며 온라인 참고 메뉴열이 존재한다.
또한 비표준 STL 컴포넌트도 제공하고 있다.

  • 헤쉬 연관 컨테이너
  • 단일 연결 리스트 컨테이너
  • 매우 긴 문자열을 담을수 있는 string 류의 컨테이너 : rope
  • 다양한 비표준 함수자 객체와 어댑터들 : select1st, select2nd, identity, project1st …
자세한 내용은 책이나 웹사이트를 참고하자.

STLport 사이트 #

[http]http://www.stlport.org/
여러가지 플렛폼에서 동작하는 STL 소스를 얻을수 있다.
특징은 SGI를 기반으로 만들어져 폭넓게 사용가능 하다는 점과 독특한 디버그 모드를 제공한다는 점이다.
자세한 예는 책을 참조하자.

Boost 사이트 #

[http]http://www.boost.org/
표준은 아니지만 곧 표준이 될만한 내용들을 담고 있는 라이브러리를 제공한다. STL을 기반으로(?) 다양한 기능을 지닌 컴포넌트들을 제공한다.

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

플렛폼마다 헤더파일의 인클루드 의존이 약간식 다를수 있다. 그러니 필요한 모든 헤더파일을 인클루드해 놓는 습관을 들여야 다른 플렛폼으로 갔을때 삽질을 피할수 있다.



  • 거의 모든 컨테이너들은 동일한 이름의 헤더에 정의되어 있다. 하지만 multiset 과 multimap은 각각 <set>, <map>에 정의 되어있다.
  • 네 개를 제외한 모든 알고리즘은 <algorithm> 에 정의되어 있다. 제외된 accumulate, inner_product, adjacent_differnce, partial_sum 은 <numeric>에 정의되이 있다.
  • istream_iterator와 istreambuf_iterator를 포함한 특별한 종류의 반복자는 <iterator>에 선언되어 있다.
  • 표준 함수자(less<T>) 함수자 어뎁터(not1, bind2nd 등)은 <functional> 에 정의되어 있다.

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

사실 이건 함수객체가 엄청난 기능이 있어서라기 보다 컴파일 타임에 코드가 구성 되는 템플릿의 특성에 기인한 것이다.즉 함수 포인터를 넘기는 경우 동일한 인자를 가진 함수들을 교체가 가능하지만 포인터로서 사용함으로써 한번의 참조가 더 일어나게 된다.

하지만 여기서 언급하는 함수객체는 STL의 대부분의 알고리즘이나 함수들이 템플릿 프로그래밍 되어 있으므로 함수객체를 코딩하고 알고리즘에 함수객체를 컴파일 타임에 결정해 줌으로써 템플릿 확장이 일어나 최적화가 되는 것이다. 마치 인라인 확장과 동일 내용이다.

물론 함수 포인터의 경우도 컴파일 타임에 분석해서 최적화를 할수 있을지 모르겠지만 현재의 컴파일러 대부분이 지원하지 않는다고 한다.

그리고 함수객체를 사용하면 컴파일러 마다 서로 약간씩 다르거나 버그 스러운 면을 자연스로럽게 비껴갈수 있다. 자세한 내용은 책을 참조하자.

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


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

위에 나열된 알고리즘들은 무언가를 찾으려고 할때 사용 가능한 것들이다. 우선 탐색범위를 알려주는 반복자쌍이 필요하다. binary_search, lower_bound, upper_bound, equal_range 는 정렬된 범위에 사용가능 하며 일반적으로 로그수행시간을 갖는다. 하지만 정렬된 범위가 아니라면 선형시간 알고리즘은 conunt, find 류의 알고리즘을 사용해야 한다.


1.1 정렬되지 않은 범위 #


1.1.1 count #


찾는값이 몇개 존재하는지 리턴.

list<Object> lw;

if(count(lw.begin(), lw.end(), w)) {

}
이런식으로 원하는 값이 존재하는지 판단할때 사용가능하다. 하지만 단지 존재하는지 만을 판단하고 싶을 경우 count는 값이 존재하더라도 몇개인지를 판단하기 위해서 끝까지 검색을 하므로 find를 사용하는것이 좋다.


1.1.2 find #


찾는값의 위치를 리턴. 없다면 end() 반복자를 리턴.
특정 범위에 원하는 값이 존재하는지를 비교하기 위해서 사용된다. 일반적으로 operator== 을 사용하며 find_if 를 사용하여 자신이 원하는 방식으로 비교하는 함수자를 사용하는 find 알고리즘을 사용할수 있다.
list<Object>::iterator i = find(lw.begin(), lw.end(), w);
if(i != lw.end()) {
// 컨테이너에 존재하는 경우
}
또한 count 와는 다르게 find의 경우는 리턴되는 반복자를 통해서 검색한 데이타의 위치를 알수 있다. 즉 범위의 끝 반복자(end반복자)와 같은 값이라면 해당 범위에 존재 검색 대상의 데이타가 존재하지 않는것이지만 끝 반복자가 아닐경우는 데이타가 존재하며 위치는 리턴된 반복자인 것이다.


1.2 정렬된 범위 #

특정 영역이나 컨테이너의 정렬은 EffectiveSTL31, EffectiveSTL34 를 참고하며 정렬에 필요한 비교 연산자에 관한 EffectiveSTL19 의 내용도 참고한다.


1.2.1 binary_search #

정렬된 범위에서 어떤 값이 존재하는지를 알아 보는데에 사용한다. C 라이브러리의 bsearch 와는 달리 binary_search는 값이 있는지 없는지만을 bool 값으로 리턴한다. 비교는 동등성 비교(operator==)를 사용.


1.2.2 lower_bound #

값을 찾는데 사용. 이 알고리즘은 원하는 값을 가진 객체의 첫째 사본의 위치를 리턴하던지 못찾았을 경우에는 그 값이 삽입될 적당한 위치를 가리키는 반복자를 리턴한다.
find 와 마찬가지로 lower_bound 의 리턴값이 원하는 값을 가리키고 있는지 검사를 해야 해당 범위안에 있는지 없는지를 판단할수 있다. 하지만 lower_bound는 상등성 비교를 하용하므로 주의할 점이 있다.
vector<Object>::iterator i = lower_bound(vw.begin(), vw.end(), w);
if( i != vw.end() && *i == w )
{
// 찾은경우
}
else
{
// 못찾은 경우
}
간단하게 생각할 경우 이런식으로 사용이 가능해 보이지만 상등성 검사이므로 operator== 를 이용해서 비교하면 문제가 생길수 있다.
상등성, 동등성에 대한 내용과 상등성 비교를 제대로 하기 위한 방법은 EffectiveSTL19 를 참조하도록 하자. 주의 할점은 lower_bound 가 사용한 비교함수와 결과검사시 사용하는 비교함수가 동일해야 한다는 점이다.(정렬할때 사용하는것도 동일해야 하지 않나?)


1.2.3 equal_range #

위의 lower_bound 가 복잡할 경우 equal_range 를 사용하도록 한다. equal_range는 반복자 쌍으로 구성된 pair 객체를 반환하는데, 첫째 반복자는 lower_bound가 반환하는 그것과 같고 둘째 반복자는 upper_bound가 반환하는 찾고자 하는 것과 동등한 값을 가진 범위의 끝의 바로 다음과 같다. 간단하게 동등한 범위를 나타내는 반복자를 리턴한다.
equal_range 의 사용하기 쉬운점은 리턴되는 두개의 반복자가 같은 값일 경우 찾을려고 했던 값이 존재하지 않는 다는 의미이다.
두번째로 distance 를 이용하여 두 반복자 사이의 거리를 구한다면 범위내의 객체의 개수를 얻을수 있다는 것이다.


1.2.4 upper_bound #

upper_bound는 위에서도 언급한 것처럼 lower_bound 와는 반대로 찾고자 하는 것과 동등한 값을 가진 범위의 끝(큰쪽)을 리턴하거나 그 값이 없다면 해당 값이 삽입되기 가장 적당한 위치를 리턴한다.
여때까지는 컨테이너에서 동일한 값을 찾는 경우를 예를 들었지만 사실 시간 같은 경우 특정 값보다 작게 되는 위치라던가 크게 되는 위치등을 찾기를 원할 경우가 있을수 있다. 이 경우 lower_bound, upper_bound 등을 사용하면 쉽게 처리가 가능하다.
Timestamp ageLimit;

vt.erase(vt.begin(), lower_bound( vt.begin(), // vt에서 ageLimit 보다
vt.end(), // 오래된 것을 모두 없앤다.
ageLimit));
만약 포함한 것을 없앨 경우
Timestamp ageLimit;

vt.erase(vt.begin(), upper_bound( vt.begin(), // vt에서 ageLimit 보다
vt.end(), // 오래되 거나 동등한
ageLimit)); // 것을 모두 없앤다.
또한 upper_bound는 동등한 값을 가진 것들을 삽입된 순서대로 저장되도록 할때 에서 쉽게 사용이 가능하다.
Object obect;

lp.insert(upper_bound(lp.begin(), // newPerson 의 앞에
lp.end(), // 오든지 동당한 객체중 가장 마지막의
newPerson, // 것 뒤에 newPerson을 삽입한다.
PersonNameLess()),
newPerson);

1.3 표준 연관 컨테이너 #

위의 lower_bound, equal_range, upper_bound 는 범위를 나타내는 두개의 반복자를 대상으로 모두 동작하며 시퀀스 컨테이너같은 경우 그대로 적용가능 하다. 하지만 연관 컨테이너의 경우는 비슷한 동작을 하는 자체적인 함수를 제공하며 당연히 그 함수들의 성능이 더욱 뛰어나다.
표준 연관 컨테이너에는 위에서 언급한 대부분의 알고리즘들이 존재하지만 binary_search 는 존재하지 않는다. 비슷한 동작을 위해서 count 를 사용할수 있다. 연속 컨테이너에서 처럼 count가 존재하는 모든 데이타를 검색한다고 생각할수 있지만 연관 컨테이너는 자체적으로 정렬되어 있는 형식이기 때문에 연속 컨테이너의 equal_range 처럼 효율적으로 동작하므로 안심하고 사용해도 좋다.


2 정리 #
















































우리가 궁금한것 사용할 알고리즘 사용할 멤버 함수
정렬되지 않은 범위 정렬된 범위 set 이나 map multiset 이나 multimap
원하는 값이 있는가? find binary_search count find
원하는 값이 있다면
첫번째 객체는 어디에?
find equal_range find find 또는 lower_bound
원하는 값의 앞에 있지 않는 값을
가진 첫째 객체는 어디에 있나요?
find_if lower_bound lower_bound lower_bound
원하는 값의 뒤에 있는 값을
가진 첫째 객체는 어디에 있나요?
find_if upper_bound upper_bound upper_bound
원하는 값을 가진 객체는 몇개 인가요? count equal_range count count
원하는 값을 가진 객체는 모두 어디에 있나요? find(계속 호출) equal_range equal_range equal_range

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

만약 컨테이너에 알고리즘을 적용할때 일반적인 알고리즘을 적용한다면 컨테이너의 특성을 고려하지 않고 동작하게 된다. 하지만 멤버함수의 경우 각 컨테이너의 구현 방식에 따른 동작을 수행하기 때문에 좀더 효율적인 수행을 할수 이다.

예를 들어 연관 컨테이너인 set에 아이템이 100만개 들어있고 find를 실행한다면 일반 알고리즘인 find는 순처적으로 검색해 나가는 반면 맴버함수인 find는 set의 기본을 이루고 있는 트리구조에 기반한 검색을 할것이다. 성능은 전자는 평균 50만. 최악의 경우 100만번의 비교가 필요하지만 후자는 최악이라 해도 40번 안쪽으로 종료가 가능하다.

또한 연관 컨테이너에서 비교에 근간을 이루고 있는 동등성과 상등성에 관한것도 일반 알고리즘을 적용시 동일하게 적용시키지 못한다.(EffectiveSTL19 참고)
게다가 map, multimap 같은 경우 일반적으로 키값만을 비교하기 원하겠지만 일반 알고리즘은 pair를 이루고 있는 아이탬 전체를 비교하여 결과값이 달라질수 있다.(물론 그걸 원할수도 있기는 하지만)

순차 컨테이너의 하나인 list의 경우 일반 알고리즘은 아이탬의 동작에 있어서 노드 단위의 복사를 수행하는 경우가 많지만 list 맴버함수는 포인터의 조작을 통해서 동작한다. 즉 효율성에서 차이가 나게 된다. 또한 동작 자체가 약간 다른 경우도 존재한다.
일반 알고리즘의 remove, remove_if는 실제로 삭제를 실행하지 않고 위치를 바꿀 뿐이며(EffectiveSTL32 참고) 실제 삭제를 위해서는 erase를 호출해야 하지만 list의 remove, remove_if, unique등의 함수는 자신이 실제로 객체를 삭제한다.

위와 같은 사실을 생각해 볼때 특정 알고리즘을 적용할 경우 컨테이너에서 제공하는 알고리즘이 존재한다면 그것을 사용하는 것이 바람직하다.

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

알고리즘은 본질적으로 루프다.

class Object{
public:

void redraw() const;

};

//…

list<Object> lw;

// 루프
for( list<Object>::iterator i=lw.begin(); i != lw.end(); i++ ){
i->redraw();
}

// 알고리즘
for_each(lw.begin(), lw.end(), mem_fun_ref(&Object::redraw));

위의 코드와 같이 대부분 루프로 가능한 일은 알고리즘으로 구현이 가능하다. 하지만 몇가지 이유에서 가능하다면 알고리즘을 사용하는것이 더 좋을때가 많다. 이유는 대략 셋






  • 효율 : 알고리즘은 프로그래머가 직접만든 루프보다 일반적으로 더 효율적이다.
  • 정확성 : 루프를 직접작성하면 알고리즘보다 에러가 일어나기 쉽다.
  • 유지보수성 : 알고리즘이 좀더 깨끗하며 간결한 코드로 작성된다.




우선 효율면에서 어째서 알고리즘이 더 좋은지를 살펴보면 루프에서 자주 호출되는 lw.end() 같은 반복자 관련 함수 호출을 생각할수 있다. 우선 이런 불필요한 함수 호출을 알고리즘 내에서는 최대한 줄이고 있다.
또한 알고리즘 자체가 STL을 만든 사람에 의해서 만들어졌으므로 좀더 STL의 구현 원리와 코드를 직접적으로 사용할수 있다. 게다가 일반 프로그래머가 따라가기 힘든 훨씬 세련된 컴퓨터 공학적인 코드로 구현되어 있다.(연속메모리 컨테이너에서 몇개의 객체를 제거하는 등의 평범한일(erase-remove 합성)조차 알고리즘이 더 효율적이라 한다.)

정확성에 관한것은 책참조
배열에 있는 값을 41씩 더해서 deque에넣는 작업을 루프와 알고리즘으로 구현.
간단히 요약하면 반복자 무효화에 대한 처리, 삽입 위치 처리, 기타 등등 루프에서 처리할게 많다! 라는 내용임
EffectiveSTL09, EffectiveSTL30 참조.

유지보수 및 명확성은 코드레벨에서 얼마나 알아보기가 쉬운가에 대한 내용이다. 즉 알고리즘은 알고리즘 이름만으로 대충 어떤 역할을 하는지 알수 있지만 루프는 루프의 코드를 보고 내용을 파악해야만 한다는 것이다.

물론 위의 내용은 상황에 따라 달라질수 있다. 알고리즘으로 구현하기 위해서는 어댑터등을 비비꼬아 사용해야 하지만 루프로는 간단히 구현 가능하다면 루프가 좋은 선택이다. 하지만 대부분의 경우에서 성능이나 명확성에 있어 알고리즘이 좀더 우위에 있다고 한다.

less는 operator< 의 의미임을 꼭 알아두자

특정 객체에 무게와 속도라는 두가지 맴버변수가 있다고 가정하자.

bool operator<(const Object& lhs, const Object& rhs)
{
return lhs.weight() < rhs.weight();
}
으로 operator<가 구현되어 있다고 할때 map, set을 사용한 map<Object>의 정렬 기준역시 operator< 를 사용한다. 즉 less<Object>는 bool operator<(const Object& lhs, const Object& rhs)를 사용한다. 라고 할수 있다.
그런데 map의 정렬기준을 무게가 아닌 속도로 정하고 싶다면 어떻게 해야 할까?
위에서 less<Object>를 언급했기 때문에
template<>
struct std::less<Object> : public::std::binary_function<Object,Object,bool>
{
bool operator()(const Object& lhs, const Object& rhs) const
{
return lhs.Speed() < rhs.Speed();
}
};
으로 less 객체를 템플릿 한정(specialize)하는 방법을 생각할수 있지만 위의 방법은 정상적으로 동작하지 않는 경우가 생길수 있다. 일반적으로 std내의 STL 컴포넌트를 수정하는 일은 금지 되어 있다.
(책에 less를 한정시켜 사용하는 경우로 스마트포인터 관련 예제가 나온다. 이 경우는 operator< 와 다른 동작을 취하도록 수정한것이 아닌 진찌 포인터 값처럼 동작하게 수정해준것에 불과하다.)
위의 방법은 C++프로그래머가 일반적으로 생각하는 less는 operator<와 동일하다고 생각하는 가정을 져버린다.

그런가?

사실 STL에서 map등의 정렬 기준을 바꾸는 일은 간단히 함수 객체를 만들어 map의 템플릿 인자로 넘겨주면 간단히 구현가능하다.
struct SpeedCompare :
public binary_function<Object, Object, bool>
{
bool operator()(const Widget& lhs, const Widget& rhs) const
{
return lhs.Speed() < rhs.Speed();
}
};

//…

multiset<Object, SpeedCompare> objects;


less는 반드시 operator<의 의미를 갖도록 하자.

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

컨테이너에 for_each 를 적용시키는 상황을 생각해보자.

void test(Widget& w);    // Widget에 대해 적용되는 전역함수

vector<Widget> vw; // Widget 컨테이너

for_each(vw.begin(), vw.end(), test);
함수객체로 실행하지 않는 경우 위와 같은 형태의 코드가 만들어 질것이다. 간단하게 생각해 볼때 for_each는
template<typename InputIterator, typename Function>
Function for_each(InputIterator begin, InputIterator end, Function f)
{
while( begin != end ) f(*begin++);
}
와 같은 형태로 수행된다. 즉 전역함수 test를 넘긴것이나 함수객체의 operator()를 오버라이드 해서 넘기는 형태인 function(item) 의 수행을 한다. 물론 당연해 보이는 구현이고 당연한 소리같다.
하지만 for_each를 사용해서 함수를 적용시킬때 전역함수나 함수객체가 아닌 클래스의 맴버함수를 적용시키고 싶다면 어떻게 해야 할까?
이때 사용할수 있는 것이 mem_fun_ref라는 함수 어뎁터이다.
// Widget 에 test라는 맴버함수가 있다고 하자
// for_each로 컨테이너의 모든 항목에 test를 적용하고 싶다.
list<Widget> lw;

for_each(lw.begin(), lw.end(), mem_fun_ref(&Widget::test) );
위와 같은 코드를 쓸수 있다. 즉 mem_fun_ref는 x.test() 로 호출되는 함수 형태를 test(x)로 호출가능한 형태로 바꿔주는 역할을 하는 것이다.(자세한 내용은 책참조..)
그렇다면 x->test() 는?? 즉 컨테이너가 객체의 포인터를 가지고 있는데 맴버함수를 적용시키고 싶다면 어떻게 해야 할까? 이럴때 쓰는것이 mem_fun 어뎁터이다.
// Widget 에 test라는 맴버함수가 있다고 하자
// for_each로 컨테이너의 모든 항목에 test를 적용하고 싶다.
list<Widget*> lpw;

for_each(lpw.begin(), lpw.end(), mem_fun(&Widget::test) );

ptr_fun의 경우는 EffectiveSTL40에도 언급되었는데 언제쓰고 언제쓰는지 헷갈린다면 함수객체가 아닌 함수포인터를 넘길경우는 모두 사용한다고 생각하면 편하다.

추가로 mem_fun 과 mem_fun_ref 의 어뎁터 이름이 이해하기 힘들게 정해진 이유는 책참조.

정리하자면









  • f(x) 형태의 함수 f를 컨테이너 전체등 STL 컴포넌트에서 사용할경우
    -> 그냥 사용하거나 ptr_fun(f) 로 넘길것

  • x.f() 형태인 맴버함수를 컨테이너 전체등 STL 컴포넌트에서 사용할경우
    -> mem_fun_ref(&CALSS::f) 로 넘길것

  • x->f() 형태로 호출될 포인터의 컨테이너에 적용될 맴버함수를 컨테이너 전체등 STL 컴포넌트에서 사용할경우
    -> mem_fun(&CLASS::f) 로 넘길것

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

list<Widget*> widgetPts;
bool isInteresting(const Widget* pw);
위와 같은 컨테이너와 함수가 있을때 find 알고리즘을 이용해서 isInteresting가 true를 반환하는 Widget 포인터 객체를 찾을수 있다.
list<Widget*>::iterator i = find_if( widgetPtrs.begin(),
widgetPtrs.end(),
isInteresting);
if( i != widgetPtrs.end() )
{
….
}
만약 not1 어댑터를 적용하여 isInteresting가 false를 반환하는 첫번째 포인터 값을 얻기 위해서는
list<Widget*>::iterator i = find_if( widgetPtrs.begin(),
widgetPtrs.end(),
not1(isInteresting));
으로 적용할수가 없다. isInteresting는 not1 이 필요로하는 typedef 문을 제공하지 않기 때문이다. 위의 코드는
list<Widget*>::iterator i = find_if( widgetPtrs.begin(),
widgetPtrs.end(),
not1(ptr_fun(isInteresting)));
으로 수정하여 정상적으로 수행이 가능하다. 즉 ptr_fun은 어댑터의 적용이 가능하도록 typedef를 정의해주는 역할을 하는 것이다.
STL의 4대 표준 함수 어댑터(not1, not2, bind1st, bind2nd)는 모두 특정 typedef가 있어야 하고 STL과 호환하는 비표준 어댑터 역시 마찬가지이다. 어댑터에서 요구하는 typedef를 제공하는 함수객체를 어댑터 적용이 가능하다 라고 표현하며 ptr_fun 은 일반 함수 포인터를 어댑터 적용이 가능한 상태로 바꾸어 주는 역할을 하는 것이다.

일단 어뎁터를 직접만드는 내용은 패스.
어쨋거나 이런 어뎁터를 적용가능한 함수객체를 만드는 방법은 직접 typedef를 써서 만들면 피곤하므로 특정 탬플릿을 상속받아 만들면 된다. 바로 std::unary_functionstd::binary_function 이다.
template<typename T>
class MeetsThreshold : public std::unary_function<Widget, bool>{
private:
const T threshold;
public:
MeetsThreshold(const T& threshold);
bool operator()(const Widget&) const;
….
};

struct WigetNameCompare : std::binary_function<Widget,Widget,bool>
{
bool operator()(const Widget& lhs, const Widget& rhs) const;
};

위의 코드에서 볼수 있듯이 상속하는 unary_function, binary_function 의 탬플릿 매개변수가 상속받는 함수객체의 operator() 함수의 매개변수와 리턴하는 변수형이다.
WigetNameCompare가 구조체인 이유는 일반적으로 내부 상태 데이터가 없는 함수객체의 경우 구조체로 선언하는 것이다.
그리고





binary_function<Widget,Widget,bool>
bool operator()(const Widget& lhs, const Widget& rhs) const;

에서 보는것 처럼 unary_function, binary_function 는 operator()가 받는 매개변수의 타입이 비포인터 타입인 경우 const 와 참조자 표시는 빼는것이 상례이다.
ex) const Widget& -> Widget

그런데 operator()가 포인터를 매개 변수로 받는 경우에는 unary_function, binary_function 의 템플릿 매개변수와 operator()의 매개변수 타입이 동일하다.
struct PtrWidgetNameCompare:
std::binary_function<const Widget*, const Widget* bool>{
bool operator()(const Widget* lhs, const Widget* rhs) const;
};

어쨋거나 결론은 함수객체를 만들때는 위와 같은 클래스를 상속받아 어뎁터적용이 가능한 객체를 만드는것이 요긴하다 라는 것이다.