- 술어구문(predicate)이란 bool값을 반환하는 함수를 말한다.
ex) find_if의 매개변수인 함수 등등
- 순수 함수란 이 함수가 반환하는 값이 그 함수의 매개변수에 종속된 함수이다.
ex) f가 순수함수고 x, y 가 객체일때 f(x,y) 의 반환값은 x나 y의 값이 바뀔때만 변한다.
- 술어 구문 클래스 란 operator()가 술어 구문인 클래스를 말한다.
함수자 클래스는 값으로 전달되도록 설계하자
STL의 함수 객체는 일반적으로 값으로 전달(복사) 된다. 하지만
for_each<vector<Object>::iterator, DoSomething&>(vi.begin(), vi.end(), d); |
함수 객체를 값으로 전달하는 방식 때문에 다형성을 구현하기 어려워 지는 단점이 있지만 이것은 프록시 클래스로 극복이 가능하다.
// 실제 구현부 함수
template<typename T>
class BPFCImpl{
private:
Widget w;
int x;
…virtual ~BPFCImpl();
// 다형성으로 구현된 함수
virtual void operator()(const T& val) const;
// 프록시 클래스가 접근가능하게 한다.
friend class BPFC<T>
};// 프록시 클래스
template<typename T>
class BPFC: public unary_function<T, void>
{
private:
BPFCImpl<T> *pImpl;public:
void operator()(const T& val) const
{
// 실제 구현객체를 호출한다.
pImpl->operator()(val);
}
};
범위 내의 데이터 값을 요약하거나 더하는 데에는 accumulate 나 for_each를 사용하자
STL 에서는 아래와 같이 특정 범위에 대한 연산을 준비해 놓고 있다.
count | 범위 내의 요소 개수를 센다. |
count_if | 조건에 맞는 요소 개수를 센다. |
min_element | 범위내의 최소값을 얻는다. |
max_element | 범위내의 최대값을 얻는다. |
하지만 위의 네가지 알고리즘 보다 좀더 융통성 있는 무언가를 해야 할 경우가 있다. 이 경우 accumulate 나 for_each 를 사용 할수 있다. 예를 들어 double 형 리스트의 총합을 알고 싶을 경우 accumulate를 아래와 같이 사용할 수 있다.
list<double> ld;
….
double sum = accumulate(ld.begin(), ld.end(). 0.0);
예를 들어 컨테이너 안에 들어있는 string 객체의 문자열 길이의 합을 계산하는데 사용가능하다.
string::size_type
stringLengthSum(string::size_type sumSoFar, const string& s)
{
return sumSoFar+s.size();
}
set<string> ss;
…
string::size_type lengthSum =
accumulate(ss.begin(), ss.end(), 0, stringLengthSum);
vector<float> vf;
…
float product =
accumulate(vf.begin(), vf.end(), 1.0, multiplies<float>() );
사용자 클래스에 대한것 역시 가능하다.
// 사용자 클래스
struct Point{
Point(double initX, double initY): x(initX), y(initY) {}
double x,y;
}// 적용될 함수자. 자세한 내용은 EffectiveSTL40을 참조
class PointAverage : public binary_function<Point, Point, Point> {
public:
PointAverage() : xSum(0), ySum(0), numPoints(0) {}
const Point operator() (const Point& avgSoFar, const Point& p)
{
++numPoints;
xSum += p.x;
ySum += p.y;
return Point(xSum/numPoint, ySum/numPoint);
}private:
size_t numPoints;
double xSum;
double ySum;
};// 실제 사용예
list<Point> lp;
…
Point avg =
accumulate(lp.begin(), lp.end(), Point(0,0) PointAverage());
// EffectiveSTL 40 참조
class PointAverage : public unary_function<Point, void> {
public:
PointAverage() : xSum(0), ySum(0), numPoints(0) {}
void operator()(const Point& p )
{
++numPoints;
xSum += p.x;
ySum += p.y;
}Point result() const
{
return Point(xSum/numPoints, ySum/numPoints);
}private:
size_t numPoints;
double xSum;
double ySum;
};// 사용예
list<Point> lp;
…
Point avg = for_each(lp.begin(), lp.end(), PointAverage()).result();
copy_if를 적절히 구현해 사용하자
STL에는 여러가지 copy 알고리즘을 제공하지만 copy_if 는 존재하지 않는다. 즉 원하는 것만을 복사를 하는 알고리즘은 직접 구현해야 한다.
template< typename IIter
typename OIter,
typename Predicate>
OIter copy_if(IIterbegin,
IIter end,
OIter destBegin,
Predicate p)
{
while(begin != end)
{
if( p(*begin) ) *destBegin++ = *begin;
++begin;
}
return destBegin;
}
대소문자를 구분하지 않는 문자열 비교는 mismatch 아니면 lexicographical_compare를 써서 간단하게 구현할 수 있다.
tolower, mismatch 를 이용해서 구현하는 방법이 있고, lexicoraphical_compare 를 이용할 수도 있다. 자세한 내용은 귀찮으므로 패스.
정렬된 범위에 대해 동작하는 알고리즘이 어떤 것들인지 파악해 두자.
특정 알고리즘들은 미리 정렬시킨 범위를 넣어주어야 한다. 만약 이러한 규칙을 지키지 않았을 경우에는 실행시 동작을 예측 할수 없게 된다.
binary_search | lower_bound | upper_bound | equal_range |
set_union | set_intersection | set_difference | set_symmetric_difference |
merge | inplace_merge | ||
include |
binary_search, lower_bound, upper_bound, equal_range 등은 내부적으로 이진 탐색을 사용해서 값을 찾기 때문에 반듯이 정렬된 데이터 범위가 필요하다.
일반적으로 위의 알고리즘들은 로그 시간의 성능을 내준다고 하지만 임의 접근 반복자를 사용할때에만 그렇다. 예를 들어 양방향 반복자등을 사용한다면 한곳에서 다른 곳으로 이동시 인접한 데이타만으로 이동이 가능하므로 선형시간 이상이 걸리게 된다.
정렬 기준에 대한 첨언
binary_search, lower_bound, upper_bound, equal_range 등의 알고리즘은 당연히 정렬된 기준과 동일한 기준으로 검색기준을 가져야 정상적으로 작동을 할것이다.vector<int> v;
…
sort(v.begin(), v.end(), greater<int>());
…
bool a5Exists = binary_search(v.begin(), v.end(), 5);
bool a5Exists = binary_search(v.begin(), v.end(), 5, greater<int>());
이와는 반대로 unique류의 알고리즘은 상등성에 의해 두 객체의 같음을 판별하게 된다.(EffectiveSTL19 참조)
remove와 비슷한 알고리즘을 포인터의 컨테이너에 적용할 때에는 각별히 조심하자.
EffectiveSTL32에서 언급한 것처럼 remove(remove_if, unique)는 지워질 데이터에 대해서 지워 지지 않는 데이타로 덮어써 버리는 동작을 취한다. 이런 동작은 컨테이너의 요소타입이 포인터일 경우 심각한 메모리 누수 현상을 일으킬 가능성이 있다.
요소를 정말로 제거하고자 한다면 remove 류의 알고리즘에는 꼭 erase를 붙여 사용하자.
remove 알고리즘의 경우 범위를 나타내는 한쌍의 반복자를 인자로 받는다. 여기서 문제가 생기는데 반복자만으로는 어떤 컨테이너 인지 알수 없을 뿐더러 해당 컨테이너를 접근할수도 없다.
그 결과 remove 알고리즘을 호출하더라 하더라도 컨테이너에 들어있는 요소의 개수가 삭제되지 않는 결과가 나타나게 된다. 이유는
romove는 어느것도 진짜로 없애지 않는다. 없앨수 없기 때문이다.
vector<int> v;
v.reserve(10);
for(int i=1; i<=10; ++i)
{
v.push_back(i);
}
v[3] = v[5] = v[9] = 99;
remove(v.begin(), v.end(), 99);
v.begin() v.end()같은 상태이다. remove를 호출한 이후는
↓ ↓
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│1 │2 │3 │99│5 │99│7 │8 │9 │99│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
v.begin() v.end()위와 같은 상태를 갖게 된다. 즉 remove는 남아있을 값을 앞쪽으로 모아 놓고 논리적인 마지막 위치의 반복자를 리턴하게 되는 것이다. 이후 remove에서 리턴한 반복자와 원래 컨테이너의 end() 반복자 사이의 요소를 컨테이너의 erase를 호출하여 삭제해줘야 한다.
↓ ↓
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│1│2│3│5│7│8│9│?│?│?│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
↑
remove에서 리턴된 반복자위치
vector<int> v;
…
v.erase(remove(v.begin(), v.end(), 99), v.end());
remove와 유사한 알고리즘으로 remove_if 가 존재하며 비슷하게 동작하는 unique(인접한 중복갑제거)도 존재한다. 즉 remove_if와 unique등은 remove와 마찬가지로 실제로 데이타를 삭제하기 위해서는 erase를 같이 호출해 주어야 한다.
list의 경우는 예외적으로 remove 와 unique 맴버함수가 존재하며 위의 설명과는 다르게 바로 데이타를 삭제한다.
위의 remove, remove_if, unique등은 삭제 대상이 되는 데이타는 신경쓰지 않고 덮어써 버리는 구조를 취하게 되므로 포인터를 컨테이너에 넣는 경우 주의해야 한다.(EffectiveSTL33)
정렬시의 선택 사항들을 제대로 파악해보자
- vector, string, deque 혹은 C++ 배열에 대해 전체 정렬을 수행할 필요가 있을 때에는 sort 나 stable 소트를 사용한다.
- vector, string, deque 혹은 C++ 배열에 대해 상위 n개의 요소만 순서에 맞추어 뽑아내고 싶다면 partial_sort를 사용한다.
- vector, string, deque 혹은 C++ 배열에 대해 상위 n개의 요소를 뽑되 순서를 고려할 필요가 없다면 nth_element가 적합하다.
- 표준 시퀀스 컨테이너가 있고, 이 컨테이너의 요소들을 어떤 기준에 만족하는 것들과 그렇지 않은 것들을 모아 구분하고 싶다면 partition 혹시 stable_partition 을 사용한다. (등급을 구분하거나 더 나아가 몇 등급이상의 것만 얻거나)
- list의 경우 partition, stable_partition 은 직접 사용이 가능하며 sort 와 stable_sort 알고리즘 대신 list::sort 멤버 함수를 사용할수 있다. 만일 partial_sort나 nth_element의 기능이 필요하다면 백터등으로 변환하거나 반복자를 백터에 넣어 정렬하는 간접적인 방법을 사용해야 한다.
- partition
- stable_partition
- nth_element
- partial_sort
- sort
- stable_sort
nth_element 의 또 다른 사용법과 다른 알고리즘의 사용예는 책을 참고
알고리즘의 데이터 기록범위는 충분히 크게 잡자
컨테이너를 대상으로 알고리즘을 수행할때는 언제나 알고리즘의 결과가 어디에 입력이 되는지를 생각해야 한다. 예를 들어
int transmogrify(int x); // 정수 x를 받아
// 새로운 값을 생성
vector<int> values;
vector<int> result;
transform(values.begin(), values.end(),
result.end(), // 알고리즘의 결과를
transmogrify); // result 뒤에 추가 시도
int transmogrify(int x);
//…
vector<int> values;
list<int> result;
transform(values.begin(), values.end(),
front_inserter(result),
transmogrify);
list 같은 의 경우 노드 단위의 컨테이너 이므로 앞,뒤 삽입이 자유로우며 삽입에 따른 오버헤드 역시 별로 없다. 하지만 임의 위치에 삽입은 탐색에 대한 오버헤드가 존재한다.
vector 등의 front_inserter를 지원하지 않고 모든 삽입의 경우 재할당 오버헤드가 발생할 가능성이 있다. 하지만 reserve 나 resize 를 이용해 재할당 오버헤드를 방지할수 있다.(물론 reserve등을 쓴다고 해도 back_inserter 등의 함수를 사용해야 반복자 위치를 정상적으로 증가 시킬수 있다.)
즉 각 컨테이너의 구현 방식에 따라 효율적인 방법을 선택해야 하는것이다.