41-1-다.객체의 셋

객체의 집합

앞 항에서는 셋의 개념을 설명하기 위해 정수만 주로 넣어 보았는데 셋은 템플릿이므로 정수뿐만 아니라 임의의 타입을 모두 넣을 수 있다. 이미 string을 저장하는 셋을 만들어 본 적이 있는데 잘 동작한다. 그러나 아무 타입이나 다 셋에 저장할 수 있는 것은 아니고 일정한 조건을 만족하는 객체만 저장할 수 있다. 셋은 키를 삽입할 때 항상 정렬된 위치에 삽입하는데 올바른 삽입 위치를 찾기 위해서는 비교 함수를 제공해야 한다. 간단한 클래스인 Time의 셋을 생성해 보자.

 

: TimeSet

#include <iostream>

#include <set>

#include <functional>

using namespace std;

 

class Time

{

protected:

     int hour,min,sec;

public:

     Time(int h,int m,int s) { hour=h;min=m;sec=s; }

     void OutTime() { printf("%d:%d:%d\n",hour,min,sec); }

     bool operator <(const Time &T) const {

          return (hour*3600+min*60+sec < T.hour*3600+T.min*60+T.sec);

     }

};

 

void main()

{

     set<Time> Times;

     Times.insert(Time(1,1,1));

     Times.insert(Time(9,8,7));

     Times.insert(Time(2,3,4));

 

     set<Time>::iterator it;

     for (it=Times.begin();it!=Times.end();it++) {

          (*it).OutTime();

     }

}

 

Time 클래스가 미만 비교 연산자인 <를 정의하고 있는데 상등 비교나 다른 부등 비교 연산자는 꼭 정의하지 않아도 상관없다. 셋의 디폴트 비교 함수 객체인 less가 요소 객체의 < 연산자로 비교를 수행하므로 일단은 < 연산자만 정의되어 있으면 된다. 실행 결과는 다음과 같다.

 

1:1:1

2:3:4

9:8:7

 

요소를 삽입할 때의 순서와는 다르게 오름차순으로 잘 정렬되어 있는데 이런 정렬이 가능한 이유는 Time이 < 연산자를 정의하여 시간 객체끼리 대소 관계를 결정할 수 있는 기능을 제공하기 때문이다. 만약 Time 클래스의 < 연산자를 주석 처리해 버리면 엉뚱한 곳에서 에러가 발생한다.

 

struct less : public binary_function<T, T, bool>

{

     bool operator()(const T& Left, const T& Right) const {

          return (Left < Right);

     }

};

 

셋의 insert 함수는 새로 삽입되는 키의 위치를 조사하기 위해 이미 삽입된 키와 비교하는데 이 과정에서 비교 함수 객체를 호출한다. 디폴트 비교 함수 객체가 less로 지정되어 있으므로 이 객체의 () 연산자가 호출되는데 이 연산자 본체에서는 셋의 요소 타입에 정의되어 있는 < 연산자로 요소끼리 비교를 수행한다. 그런데 Time이 < 연산자를 제공하지 않으므로 에러가 나는 것이 당연하다.

함수 객체 사용

Time 객체를 셋에 저장하기 위해서는 < 연산자를 제공해야 하는데 이는 셋의 비교 함수 객체인 less가 이 연산자를 요구하기 때문이다. 만약 set의 비교 객체를 greater로 변경한다면 이때는 < 연산자가 아닌 > 연산자를 정의해야 한다. 앞의 예제를 다음과 같이 수정하면 오름차순으로 정렬될 것이다.

 

class Time

{

     ....

     bool operator >(const Time &T) const {

          return (hour*3600+min*60+sec > T.hour*3600+T.min*60+T.sec);

     }

};

 

void main()

{

     set<Time, greater<Time> > Times;

     ....

}

 

오름차순이든 내림차순이든 어쨌든 정렬만 가능하면 셋에 저장될 수 있다. 셋이란 정렬된 자료를 기반으로 중복 제거와 빠른 검색을 제공하는 컨테이너이므로 목적을 이루기 위해서는 비교가 필수적이다. less, greater 같은 미리 제공되는 함수 객체 대신 사용자가 직접 만든 함수 객체 타입을 지정할 수도 있다.

 

: TimeComp

#include <iostream>

#include <set>

#include <functional>

using namespace std;

 

class Time

{

     friend struct TimeComp;

protected:

     int hour,min,sec;

public:

     Time(int h,int m,int s) { hour=h;min=m;sec=s; }

     void OutTime() { printf("%d:%d:%d\n",hour,min,sec); }

};

 

struct TimeComp : public binary_function<Time,Time,bool>

{

     bool operator()(const Time &a, const Time &b) const {

          return (a.hour*3600+a.min*60+a.sec < b.hour*3600+b.min*60+b.sec);

     }

};

 

void main()

{

     set<Time,TimeComp> Times;

     Times.insert(Time(1,1,1));

     Times.insert(Time(9,8,7));

     Times.insert(Time(2,3,4));

 

     set<Time,TimeComp>::iterator it;

     for (it=Times.begin();it!=Times.end();it++) {

          (*it).OutTime();

     }

}

 

TimeComp라는 함수 객체 클래스를 정의하고 이 클래스의 () 연산자가 두 개의 Time을 받아 대소를 비교한다. TimeComp가 Time의 프라이비트 멤버를 읽어야 하므로 Time은 이 클래스를 프렌드로 지정했다.

set 템플릿의 두 번째 인수로 TimeComp 타입을 지정하면 set의 디폴트 생성자는 이 타입의 디폴트 생성자를 호출하여 비교 객체를 하나 만들고 비교가 필요할 때마다 이 함수 객체의 () 연산자를 호출할 것이다. () 연산자가 앞 예제의 < 연산자와 동일한 방법으로 Time 객체를 비교하므로 실행 결과는 일단 동일하다.

그러나 함수 객체는 다양한 방법으로 객체들을 비교할 수 있다는 면에서 훨씬 더 유연하다. 시간이야 별 다른 비교 방법이 더 없겠지만 문자열의 경우 대소를 구분할 것인지 아닌지, 한글이 먼저인지 영문이 먼저인지 등의 규칙을 나름대로 지정할 수 있다. 또한 성적 구조체를 정렬해 넣는다면 총점이 같을 경우 표준편차가 작은쪽에 더 높은 점수를 주는 식으로 이차 정렬 기준을 지정할 수도 있다.

셋은 원하는 비교 방법을 마음대로 지정할 수 있도록 두 번째 템플릿 인수로 사용자가 만든 함수 객체를 받아들이며 이 객체의 비교 방식을 그대로 따른다. 템플릿의 인수로 전달되어야 하므로 단순한 함수 포인터는 사용할 수 없으며 반드시 타입으로 정의되는 클래스여야 한다. set은 비교 함수 객체를 생성해서 가지고 있다가 비교할 때 객체의 () 연산자 함수를 호출한다. 함수 객체가 함수 포인터에 비해 이런 면이 더 우월하다.

객체의 포인터 집합

셋에 임의의 타입을 요소로 저장할 수 있다고 했으므로 객체의 포인터도 셋에 저장할 수 있다. 포인터는 기본적으로 비교 연산을 제공하므로 셋의 요소가 되기 위한 요건을 만족하기는 하지만 객체의 번지를 기준으로 정렬하는 것은 실용성이 없으며 포인터가 가리키는 곳의 실제 객체를 기준으로 정렬해야 한다. 이때도 사용자가 비교 함수 객체를 직접 제공해야 한다.

예를 들어 Time형 객체가 아닌 Time의 포인터 집합을 셋에 저장한다고 해 보자. TimeSet 예제의 main 함수 코드만 다음과 같이 수정해 보자.

 

void main()

{

     set<Time *> Times;

     Times.insert(new Time(1,1,1));

     Times.insert(new Time(9,8,7));

     Times.insert(new Time(2,3,4));

 

     set<Time *>::iterator it;

     for (it=Times.begin();it!=Times.end();it++) {

          (**it).OutTime();

     }

     for (it=Times.begin();it!=Times.end();it++) {

          delete *it;

     }

}

 

new 연산자로 Time 객체를 동적 할당하여 셋에 삽입하고 종료 전에 셋의 요소를 순회하면서 delete로 직접 삭제했다. 벡터와 마찬가지로 셋도 요소만 관리할 뿐 요소가 가리키는 실체까지 관리하지는 않는다. 셋에 저장된 타입이 Time*이므로 반복자가 가리키는 곳에는 Time *가 있을 뿐이다. 그래서 출력할 때도 **it로 두 번 참조해야 Time 객체를 읽을 수 있다. 무사히 컴파일은 되지만 실행 결과는 예상대로 되지 않는다.

 

1:1:1

9:8:7

2:3:4

 

정렬이 제대로 되지 않았는데 왜냐하면 포인터의 대소 관계만을 따져 정렬하기 때문이다. 일반적으로 먼저 생성된 객체가 낮은 번지에 있을 확률이 높으므로 생성된 순서대로 셋에 삽입되었는데 어디까지나 확률일뿐이므로 실제 어떤 번지에 할당되는가에 따라 결과는 매번 달라질 것이다. 이렇게 되면 검색할 때도 포인터로 검색을 하므로 완전히 동일한 같은 번지에 있는 객체가 아닌 한은 검색되지 않는다.

우리가 원하는 바는 포인터의 번지를 기준으로 정렬하는 것이 아니라 포인터가 가리키는 곳에 있는 Time 객체의 실제값으로 정렬하는 것이다. 디폴트 비교 객체인 less는 셋 요소 타입의 < 연산자만으로 비교하므로 less로는 원하는대로 정렬할 수 없다. 포인터가 가리키는 실제 요소값으로 정렬하는 비교 함수 객체를 작성해야 한다. 완성된 예제는 다음과 같다.

 

: TimePtrSet

#include <iostream>

#include <set>

#include <functional>

using namespace std;

 

class Time

{

protected:

     int hour,min,sec;

public:

     Time(int h,int m,int s) { hour=h;min=m;sec=s; }

     void OutTime() { printf("%d:%d:%d\n",hour,min,sec); }

     bool operator <(const Time &T) const {

          return (hour*3600+min*60+sec < T.hour*3600+T.min*60+T.sec);

     }

};

 

struct Timeless : public binary_function<const Time *,const Time *,bool> {

     bool operator() (const Time *A, const Time *B) {

          return *A < *B;

     }

};

 

void main()

{

     set<Time *, Timeless> Times;

     Times.insert(new Time(1,1,1));

     Times.insert(new Time(9,8,7));

     Times.insert(new Time(2,3,4));

 

     set<Time *, Timeless>::iterator it;

     for (it=Times.begin();it!=Times.end();it++) {

          (**it).OutTime();

     }

     for (it=Times.begin();it!=Times.end();it++) {

          delete *it;

     }

}

 

Timeless 함수 객체의 () 연산자는 Time형 포인터 둘을 인수로 전달받아 포인터가 가리키는 곳의 실체끼리 비교한다. Time 클래스가 < 연산자를 정의하고 있으므로 포인터에 * 연산자를 적용한 객체끼리 < 연산자로 직접 비교했다. Time에 < 연산자를 삭제하고 Timeless가 직접 비교할 수도 있으나 이렇게 하려면 프렌드 지정이 필요하다.