이번 글에서는 제너릭 알고리즘에 대한 이야기입니다. 우선 간단하게 제너릭 알고리즘 중 merge 함수의 바른 사용법에 대해서 이야기해보고 본론으로 들어가서 컨테이너와 제너릭 알고리즘에 대해서 이야기 해보겠습니다.
merge 함수 바르게 알고쓰기
merge 함수는 두 컨테이너의 반복자를 받아서 대상 컨테이너에 순서대로 요소를 추가하는 함수입니다. 그런데, merge 함수의 5번째 파라미터로 넘겨줄 대상 컨테이너가 문제입니다. merge 함수는 대상 컨테이너에 요소를 채워넣을 때 이미 충분한 공간이 할당되어있다고 가정합니다. 아래와 같은 코드는 예외가 발생합니다.
[리스트 1]
list container1 ;
for ( int i=0 ; i < 5 ; ++i ) {
container1
.push_back(tr1::shared_ptr(new Point(i,i))) ;
}
list container2 ;
for ( int i=0 ; i < 5 ; ++i ) {
container2
.push_back(tr1::shared_ptr(new Point(i,i))) ;
}
list mergeContainer ;
merge(container1.begin(), container1.end(),
container2.begin(), container2.end(),
mergeContainer.begin()) ; // 여기서 예외가 발생합니다
merge 함수를 사용할 때는 대상 컨테이너의 크기를 미리 확장시켜 놓거나 다음과 같이 반복자 어댑터를 사용해야 합니다.
[예제 1]
merge(container1.begin(), container1.end(),
container2.begin(), container2.end(),
back_inserter(mergeContainer)) ;
컨테이너에는 제너릭 알고리즘이 제격
STL 컨테이너를 사용하면서 겪었던 가장 큰 불편은 무었일까요? 바로 컨테이너를 바꿀 때 수정해야하는 수많은 코드들입니다. STL의 장점이 제너릭 프로그래밍인데 컨테이너 하나 바꿨다고 코드를 줄줄이 변경해야 하다니 어떻게 된 노릇일까요? 저도 이 문제 때문에 고민을 많이 해봤지만 다른 개발자들 역시도 많은 고민을 했을겁니다.
오죽하면 Effective STL에서는 제너릭하게 컨테이너를 사용하기 위한 고민을 더 이상 하지 말라고 따로 항목까지 마련했을까요. 그런데 컨테이너를 제너릭하게 사용할 수 있는 방법이 있습니다.
바로 제너릭 알고리즘을 사용하는 것입니다. 컨테이너를 제너릭하게 사용할 수 없는 이유는 반복자때문입니다. 그리고 반복자를 사용해야하는 이유는 대부분 컨테이너 내부의 객체를 순환해야 하는 경우입니다.
C++는 정적 언어이기 때문에 컨테이너의 요소를 참조하기 위해서는 확실한 타입을 지정해주어야 하고, 반복자는 컨테이너마다 다른 타입으로 지정되어 있기 때문에 컨테이너가 변경되면 반복자의 타입 역시도 변경되어야 하므로 연관되는 모든 소스를 변경할 수 밖에 없습니다. 사실 그 이외의 경우는 typedef로 해결할 수 있고 코드를 수정하는 부담이 그리 크지 않습니다.
예를 들어 컨테이너의 포인터 객체를 해제해야 한다면 for_each와 함수 객체를 사용하세요. 사실 스마트 포인터를 사용하는 것이 더 효과적이긴 합니다. 여러 컨테이너를 하나로 합치고 싶다면 앞서 설명한 merge 함수를 사용하면 됩니다. find 함수를 사용해야 하는데 원하는 조건이 없다면 사용자 predicate 함수 객체를 작성하면 됩니다.
제너릭 알고리즘을 사용하면 컨테이너를 사용하는 것이 즐거워집니다. 반복되는 코드도 없고 코드도 훨씬 간결해집니다. 컨테이너가 바뀌어도 코드를 일일이 수정하지 않아도 됩니다. 원하는 기능을 하는 알고리즘이 없는 경우는 템플릿으로 제네릭 알고리즘을 작성하면 됩니다.
함수 객체와 좀 더 친해진다면 STL이 많이 편해질 겁니다. C++0x에서는 람다 함수가 추가되어 함수 객체 없이도 predicate을 정의하기가 더 편해졌습니다. 그러나 모든 컴파일러가 아직은 람다 함수를 지원하는 것은 아니므로 여전히 함수 객체와 좀 더 친해질 필요는 있습니다.