Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

  • 참고 문서 : 템플릿 사용 문서 STL(1) - 개요

    STL(1) - 개요

    윤 상배

    dreamyun@yahoo.co.kr



    1절. 소개

    그동안 이사이트의 몇개 문서에서 STL 에 대한 언급이 있었지만, 매번 그냥 이런것이 있다.. 라는 정도로 하고 넘어갔었다. 이번에는 STL에 대한 직접적인 내용을 다루려고 한다.

    STL 에 대해서 제대로 다루기 위해서는 C++ 에 대한 기본적인 이해와 템플릿 에 대한 이해를 필요로 한다. (템플릿, 클래스, 오버로딩, 함수포인터 등에 대한 이해를 필요로 한다.) 그리고 STL 전체를 제대로 다루려면 상당히 많은 시간을 투자해야 한다. 보통 STL 주제만으로도 책 한두권 정도는 간단하게 나올것이다(실제로 STL을 주제로한 몇권의 책이 시중에 나와있다).

    그러므로 STL 에 대한 강좌는 몇번의 기사에 나누어서 이루어지게 될것이다. 이번 기사는 STL 에 대한 소개와 템플릿 프로그래밍에 대한 내용들을 다루게 될것이고, 다음번의 기사는 "sequences 컨테이너", "associative 컨테이너", "Iterator", "알고리즘", "그밖의 STL 에 대한 기술적인 다른 내용들" 이 될것이다. 전체적으로 5-6회에 걸쳐서 연제에 들어가게 될것이다.


    2절. STL 소개

    2.1절. STL 이란 무엇인가

    STL 은 Standard Template Library 라고 불리우며, C++ 에서 사용할수 있는 컨테이너(container) class 와, 알고리즘 일반화 시켜서 사용할수 있는 자료구조 등을 포함하는 라이브러리의 모음이다. STL 은 흔히 generic library(일반화된 라이브러리) 라고 불리우며, 이러한 일반화를 구현하기 위해서 C++ 에서 제공하는 Template(템플릿) 을 사용하고 있다.


    2.1.1절. 왜 STL 을 사용하는가

    전용의 함수 혹은 전용의 자료구조가 아닌 일반적으로 사용 가능한 함수 혹은 자료구조를 만들기 위해서이며, 이러한 일반적인 자료구조에 일반적인 알고리즘을 적용시키기 위해서 이다.

    말이 복잡한데 간단히 말해서, 프로그래밍 시간을 극적으로 단축시키고 코드수를 줄이고 유지보수 쉽고 확장 쉬운 코드를 만들기 위해서이다. 한마디로 인생을 편히 즐기기 위해서 STL 을 사용한다. 예를들어 여러분이 queue 자료구조를 구현한다고 해보자. 머 전산수업시간에 충실히 했다면, 그리고 시중에 나와있는 몇가지 책들을 열심히 섭렵했다면 만들수는 있겠지만, 개인적인 생각인데, 최소한 수백라인 이상의 코드를 그려내어야 할것이다. 코드수가 늘어났으니 그만큼 버그일어날 확률도 많고, 이것저것 잡다하게 신경써야할게 한두개가 아닐것이다. 게다가 다른 자료구조를 지원하도록 해야 한다면, 그때 마다 각각의 자료구조를 위한 새로운 전용의 함수들을 생성해 내야만 할것이다. 매크로를 이용해서 혹은 클래스를 이용해서 어느정도 흉내내기는 가능하겠지만, 이건 어디까지나 흉내일 뿐이다.

    이걸 STL 에서 제공하는 deque 를 이용하면 10줄 내외로 작성 가능하다. 크기가 얼마나 되어야 할지 걱정할 필요도 없고, 메모리 뻑나지 않을지, queue 를 어떻게 구성해야 할지에 대해서 고민할 필요 없이 그냥 선언해서 사용하면 된다. 게다가 표준이다. Linux 에서 작성한거 그대로 솔라리스에서 컴파일만 하면 제대로 작동한다. Windows 에서도 마찬가지이다(윈도우의 경우 STL 라이브러리를 별도로 설치해야 하는걸로 알고 있다).

    예제 : queue.cc

    #include <deque>
    
    int main()
    {
    	deque<int> myque;	
    
    	myque.push_back(100);
    	myque.push_back(200);
    	cout << myque[0] << endl;
    	myque.pop_front();
    	myque.push_back(300);
    	myque.push_back(400);
    	cout << myque[0] << endl;
    	myque.pop_front();
    	cout << myque[0] << endl;
    
    	cout << "Size " << myque.size() << endl;
    }
    				
    위의 코드를 보시라 실질적으로 보면 queue 자료구조를 위해서 단지 한줄만을 사용하고 있음을 알수 있다. 단지 한번의 선언으로 queue 자료구조를 만들었다. 만약 큐에 저장할 자료가 문자열이여야 한다면 ? deque<char *> mychar; 이 한줄로 끝이다. 구조체, 클래스 는 물론이고 다른 STL 컨테이너 까지도 멤버 자료로 가질수 있다.

    위의 queue 같은 경우는 STL 을 사용하지 않더라도 그마나 구현이 용이하다. 그렇지만 map(인덱스가 int 형이 아닌 스트링같은) 같은걸구현하려고 한다면 이거 보통일이 아닐것이다. 이것역시 STL 을 이용하면 선언으로 끝이다.

    결론적으로 말해서 STL을 사용하면 그렇지 않을때 보다 50% 이상의 시간절약이 가능하다. 게다가 아드레날린의 생성도 획기적으로 줄여준다. 특히 데이타 저장과 자료구조를 위한 container 의 제작과 사용에 있어서 획기적인 방법을 제공해 준다.


    2.1.2절. STL 은 어떻게 구현되었는가

    STL 은 클래스를 통해서 이루어진게 아니다 (STL 템플릿 들도 클래스로 만들어 졌다. 여기에서 말하는 것은 모든 기능의 구현을 위해서 클래스만을 전적으로 사용한게 아니란 뜻이다. STL에서 클래스는 거의 메서드와 자료형을 함께 두기 위한 구조체 수준에서 사용된다.). C++ 에서 제공하는 Template 를 이용해서 이루어졌다. 이 template 에 대해서는 따로 장을 할당해서 설명할것이다.


    2.1.3절. STL의 장단점

    모든게 그렇지만 STL 역시 장점과 더불어 몇가지의 단점을 가지고 있다.


    2.1.3.1절. 장점

    장점으로는 우슨 소스크기의 축소를 들수 있다. 이미 STL에는 자주 사용되는 50여개 정도의 알고리즘과 다양한 데이타 구조들을 가지고 있다. 우리는 이러한 STL에서 사용하는 자료구조와 알고리즘을 이용해서 쏘스 코드의 크기를 획기적으로 줄일수 있다.

    STL 알고리즘과 컨테이너들은 기존의 C/C++ 의 포인터와 배열에도 사용할수 있으며, 변환 가능하므로 유연하게 사용가능하다. 그리고 컨테이너와 알고리즘이 서로 분리되어 있음으로(많은경우), 자신이 알고리즘을 만들어서 컨테이너에 적용시키는 작업이 가능하다.

    대부분의 알고리즘과 자료구조들은 충분히 테스트되고 최적화 되어 있다. 또한 효율적인 프로그래밍을 위한 다양한 접근 방법을 제공하므로, C++ 을 사용한 STL 이 느릴것이라는 예상을 깨고 약간만 신경을 쓴다면 매우 효율적인 코드를 생성할수 있다.


    2.1.3.2절. 단점

    우선 디버깅이 어렵다. STL을 잘못사용할경우 발생되는 컴파일시의 에러코드는 템플릿 깊숙이에 있는 내부함수들의 에러들을 출력시킨다. 이러한 에러가 발생하면 일단 에러메시지의 양들도 많을 뿐더러, 이해자체가 어려운 메시지들이다. 이걸 해결하는 방법은 하나뿐이다. STL 에 익숙해지기..

    STL이 유연성을 구현하기 위한 템플릿의 경우 프로그램의 크기를 매우 크게 만들수 있다. 아직은 컴파일러가 템플릿을 그리 효율적으로 처리하지 못하기 때문이다. 역시 비용을 잘 고려하여서 효율적으로 STL의 알고리즘과 컨테이너들을 사용해야 한다.

    반복자(나중에 다룰것임, 일종의 포인터)와 컨테이너가 분리되어서 존재한다. 이는 쓰레드 프로그래밍시 문제를 발생할 소지를 가지고있다.


    3절. Template

    사실 템플릿도 그리 만만한 내용이 아니다. 좀두꺼운 C++ 책같은 경우 왠만한 얇은 책 한권 분량 정도를 할애해서 템플릿을 설명한다. 이 문서는 템플릿 자체를 설명하는 문서는 아니므로 그렇게 자세히 설명하지는 않을 것이다. 다만 STL을 좀더 쉽게 이해하도록 돕는 수준에서만 설명을 할것이다. 템플릿에 대한 자세한 내용은 다른 문서나, 책을 이용하도록 하자. (비록 C 와 C++ 을 다른 언어라고 말을 하긴 하지만) C 에 비해서 C++ 에서 크게 추가된 것이 있다면 바로 "클래스" 와 "템플릿" 정도가 될것이다.


    3.1절. Template 에 대해서

    만약에 우리가 int 형의 숫자를 받아들이는 A 라는 함수를 만들었다고 가정을 하자. 이 A 라는 함수는 int 아규먼트를 받아들일 경우는 문제가 없지만, 이 A 라는 함수는 단지 int 형만을 처리할수 있다는 문제점을 가지게 된다. float, double 등이 인자로 들어오게 되면 제대로 처리를 할수 없을것이다. 이럴경우 어쩔수 없이 전용의 새로운 함수를 만들어야만 할것이다. 물론 C++ 의 특성중 하나인 멤버함수의 오버로딩을 이용하면 어느정도의 이러한 문제를 해결할수 있을것이다. 그러나 만약 struct 나, string, class 등을 처리하는 함수를 만들어야 한다면? 이쯤 되면 함수 오버로딩 정도로 그리 간단하게 해결될 문제가 아니다(프로그래밍에서 대부분의 것들이 그렇듯이 물론 불가능한건 아니다. 하지만 너무 많은 비용이든다. 너무 많은 시간, 너무 많은 노력을 필요로 한다)

    어쨋든 Template 이 아규먼트로 주어지는 형에 관계없이 받아들일수 있다는 것 때문에, 특히 데이타 저장소(container)로 유용하게 쓰인다. STL 역시 Template 의 이런 특성을 이용해서 범용적으로 사용가능한 저장소를 제공한다(vector, map, list, queue 등등..)

    STL 에서는 보통 이러한 저장소를 만들기 위해서 class template 를 이용하며 알고리즘을 위해서 함수 template 를 이용한다. 저장소를 위해서 class template 를 이용하는 이유는 class 가 데이타부분과 메써드 부분을 분리할수 있음으로 아무래도 데이타를 저장하기가 방법상 더 간단하기 때문이다. 알고리즘을 이용해서 함수 template 를 이용하는 이유는 template 의 특성을 이용해서 인자에 관계없이 범용적으로 사용가능한 알고리즘을 만들수 있기 때문이다.


    3.2절. 템플릿 작성

    3.2.1절. 함수 템플릿

    작성방법은 간단하다. C++ 에서는 템플릿의 제작을 위해서 "template" 라는 키워드를 제공해준다.

    예제 : myadd_template.cc

    #include <string>
    
    template <typename T>
    T myadd(T x, T y)
    {
    	return x + y; 
    }
    
    
    int main()
    {
    	cout << myadd(4, 6) << endl;
    	cout << myadd(1.6, 1.8) << endl;
    }
    				
    위의 예제를 테스트 해보면 데이타 형에 관계없이, '+' 연산이 가능함을 알수 있다. 즉 myadd(2, 3), myadd(3.5, 5.98) 등 int, float 등의 데이타 형에 신경쓸필요가 없이 연산이 가능하다. 여기에는 '+' 연산자를 사용할수 있는 모든 자료형이 사용가능하다. 예를 들어 STL 에서 제공하는 string 의 경우 문자열간 의 '+' 연산이 가능하다. 그러므로 다음과 같이 문자열을 더하기 위해서도 사용할수 있을것이다.
      
    string b2="hello";
    string b3=" world";
    cout << myadd(b2, b3) << endl;
    				
    그러나 char * 형같은경우는 '+' 연산을 취할수 없기 때문에 char *을 인자로 취할경우에는 연산자 오류를 발생시키며 컴파일 실패할것이다.

    그리고 위의 예제는 또다른 문제를 가지고 있다 typename T 가 서로 같은 종류의 형을 가져야 된다는 것이다. (int, int), (float, float), (string, string) 등을 사용하는데 있어서는 문제가 없지만 (int, float) 와 같이 서로 다른 형을 사용할때는 문제가 발생할것이다. 외냐하면 typename T 에 대해서 이러한 아규먼트는 지원이 되지 않기 때문에, 위의 아규먼트를 처리할수 있는 어떠한 함수원형도 발견할수 없다는 오류 메시지를 출력하며 컴파일 에러가 발생할것이다.

    이럴경우에는 연산자 오버로딩을 통하여 해결 가능하다. 다음과 같은 연산자 '+' 에 대해서 정의를 해두면 된다.

    bool operator+(myadd<double, long> &x)
    {
        return x.first + x.second;
    }
    				
    물론 이경우에는 함수 템플릿으로는 안되고, 클래스 템플릿을 작성해야 할것이다.


    3.2.2절. 클래스 템플릿

    클래스 템플릿의 사용역시 매우 간단하다. 아래는 바로 윗 절에서 사용되었던 예제를 연산자 오버로딩 까지 포함시켜서, double, long 연산까지 가능하도록 약간 수정한 것이다.

    예제 : class_tem.cc

    #include <string>
    
    template <typename T1, typename T2>
    class cal
    {
        public:
            bool operator+(cal<double, long> &x)
            {
                return x.first + x.second;
            }
    
            void sum(T1 x, T2 y)
            {
                cout << x + y << endl;
            }
    
            void dif(T1 x, T2 y)
            {
                cout << x - y << endl;
            }
    
    };
    
    int main()
    {
        cal<int, int> data;
        data.sum(1, 2);
        data.sum('a', 'b');
    
        cal<double, long> data2;
        data2.sum(1.2, 2);
    
        cal<double, double> data3;
        data3.dif(1.2, 1.5);
    }
    				

    하지만 클래스 템플릿의 진정한 사용용도는 데이타 저장소(container)를 만들기 위한 용도이다. 만약 queue 형식을 지원하는 데이타 저장소를 만들고 지원해야할 데이타 형의 종류가 여러가지라면, 우리는 데이타 형의 종류의 수만큼의 함수혹은 클래스를 만들어야 할것이다. 하지만 클래스 템플릿을 사용하면 이러한 수고를 덜수 있다. 템플릿을 이용한 container 의 제작을 위해서 다양한 데이터형을 저장할수 있는 arrary 데이타 구조를 가지는 어떤 저장소를 만들어 보도록 하겠다.

    우리는 클래스 템플릿 를 이용해서 circular buffer 구조를 가지는 queue 자료구조를 구현할것이다. circular buffer 는 배열을 이용한 buffer 로 다음과 같은 구조를 가진다.

        +- data1    +- data3                      +- data8  
        |           |                             | 
     +-----+-----+-----+-----+-----+-----+-----+-----+
     |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |
     +-----+-----+-----+-----+-----+-----+-----+-----+
        |     | 
        |     +- data2
        +- data9
    				
    이것은 고리처럼 순환(circular) 하는 구조로 8개까지의 배열을 다채우고 나면 다시 배열의 처음으로 이동해서 값을 채우는 식으로 계속 순환 한다. ring 구조라고 보면 될것이다. 이 circular buffer 는 매우 구현하기가 쉬우며 특히 queue 를 구현하는데 적합하다. 물론 배열의 크기에 제한이 있을수 있음으로 배열의 크기결정에 신중해야 하며, 아직 읽지 않은 데이타를 덮어쓰지 않도록(한바뀌를 돌았을경우) 안전장치를 마련해두어야 할것이다. 가장 간단하게 구현가능한 안전장치는 데이타를 넣고 읽을때 마다 현재 데이타의 총 갯수를 계산하는 방법일것이다. 그러나 이경우에는 총 갯수를 계산하기 위해서 바쁜 대기 상태에 놓일 가능성이 있음으로 이와 함께 Pthread 의 뮤텍스와 조건변수를 이용하는게 더좋은 방법이 될것이다. 혹은 세마포어를 이용할수도 있을것이다.

    이번예제는 (버퍼에)읽기전용 쓰레드와 쓰기전용 쓰레드로 구성된 프로그램을 만들것이다. 그리고 읽기/쓰기 동기화를 위해서 뮤텍스와 조건변수를 사용하게 될것이다. 쓰레드와 뮤텍스 조건변수에 관련된 내용을 이사이트의 다른 문서들을 참고하기 바란다.

    이 프로그램은 2개의 파일로 이루어질것이다. 하나는 container 제작을 위한 템플릿 클래스를 가지는 buff.h 다른 하나는 main 함수를 포함하는 main.cc 이다.

    예제 : buff.h

    #include <string>
    #include <pthread.h>
    
    template <typename T>
    class array
    {
        private:
            // 저장소의 배열크기는 10으로 고정시킨다.  
            T container[10];
            int member_num, index_num;
    
            pthread_mutex_t mutex;
            pthread_cond_t  thread_cond;
        public:
    
            // 생성자
            // mutex 잠금과, 조건변수의 사용을 위한 초기화를 한다.
            array()
            {
                member_num = 0;
                index_num = -1;
                pthread_mutex_init(&mutex, NULL);
                pthread_cond_init(&thread_cond, NULL);
            }
    
            // 저장소 container 에 데이타를 
            // 입력하기 위한 함수 
            // 만약 10번째 배열까지 다 썼다면 
            // 다시 0번째 배열부터 쓰기 시작한다. 
            void push_back(T x)
            {
                pthread_mutex_lock(&mutex);
                cout << "push --> " << member_num%10 << endl;
                container[member_num%10] = x;
                member_num ++;
                pthread_cond_signal(&thread_cond);
                pthread_mutex_unlock(&mutex);
            }
    
            // 저장소에 있는 데이타를 읽어온다. 
            // 가장최근의 데이타를 가져오기 위해서 
            // 참조할 index 값을 가지는 변수 index_num 을 
            // 사용한다. 
            // pop() 를 한번 호출하면 index_num 을 1 증가 시켜서 
            // 다음헤 호출할때는 다음 데이타 값을 가져오도록 한다.  
            // 만약 저장소에 들어있는 데이타의 크기가 0이라면 
            // cond_wait 로 시그널을 기다린다. 
            T pop() 
            {
                T return_container;
                pthread_mutex_lock(&mutex);
                if (size() == 0)
                {
                    pthread_cond_wait(&thread_cond, &mutex);
                }
                index_num++;
                return_container =  container[index_num%10];
                pthread_mutex_unlock(&mutex);
    
                return return_container;
            }
    
            // 저장소의 크기를 되돌려준다. 
            int size()
            {
                return member_num - (index_num + 1);
            }
    };
    				

    예제 : main.cc

    #include "arr_tem.h"
    #include <string>
    #include <stdio.h>
    #include <unistd.h>
    
    typedef struct data
    {
        int     data_num;
        string  name;
    } man_info;
    
    array<struct data> buff;
    
    void *push(void *data)
    {
        int i = 0;
        char name[12];
        man_info info;
    
        while(1)
        {
            i++;
            info.data_num = i;
            sprintf(name, "man_%d", i);
            info.name = name;
            buff.push_back(info);
            if (buff.size() == 10)
            {
                cout << "대기인원을 초과했습니다. 잠시 기다려주세요." << endl;
                sleep(12);   
            }
            else
            {
                sleep(random()%5);
            }
        }
    }
    
    void *pop(void *data)
    {
        man_info info;
        while(1)
        {
            info = buff.pop(); 
            cout << "현재 대기 인원 : " << buff.size() << endl;
            cout << info.data_num << " : " << info.name << endl;
            sleep(3);
        }
    }   
        
    int main()
    {
        int thr_id;
        int a, b, status;
        pthread_t p_thread[2];
    
        thr_id = pthread_create(&p_thread[0], NULL, push, (void *)&a);
        thr_id = pthread_create(&p_thread[1], NULL, pop, (void *)&b);
    
        pthread_join(p_thread[0], (void **)status);
        pthread_join(p_thread[1], (void **)status);
    }
    				

    컴파일 방법은 다음과 같다.

    [root@localhost circular]# g++ -o main main.cc -lpthread
    				

    위의 예제는 면접예제이다. 면접을 보기 위해서 피면접자는 대기실에서 기다리고 있어야 한다. 대기실의 정원은 10명이므로 10명이상이 들어오지 못하게 대기실밖에서 관리를 해주어야 한다. 한사람이 면접보는데 걸리는 시간은 3초인 반면 면접보러 오는 사람은 0-5초 간격으로 랜덤하게 도착한다. 그러므로 대기실관리를 해주지 않으면 언젠가는 대기실이 꽉차버릴 것이다. 그걸 막기 위해서 사람을 받아들일때 마다. 대기실의 크기를 조사해서 꽉찼다면 15초 동안은 무조건 진입시키지 않도록 했다. 바쁜 대기를 막기 위해서 size 가 0일경우에는 조건변수로 signal 이 전달될때까지 기다린다.

    위의 예제는 circular buffer 를 이용한 queue를 이용해서 구현했다. 지금은 면접자의 정보를 위해서 구조체를 사용하고 있지만 여러분이 마음만 먹는다면 int 형, 클래스, char *형 을 간단히 선언만 함으로써 사용할수 있다.

    위의 예제는 queue 가 꽉차면 무조건 12초를 기다리게 되어 있는데, 이왕이면 queue 가 비는 즉시 다음 데이타를 받아들이도록 하는게 좀더 효율적일 것이다. 이것은 역시 조건변수를 이용하면 가능하다. 이것은 간단하니 각자 구현해보도록 한다. 그리고 배열의 크기를 10으로 고정시켜 놨는데, 이왕이면 new 나 malloc 으로 할당할수 있도록 만드는게 좋을것이다. 시간이 남으면 총 면접자를 지정하고 모든 면접자에 대한 면접이 끝나면 종료하도록 수정해 보도록 하자.

    이처럼 클래스 템플릿 을 사용하면 다양한 데이타형을 저장할수 있는 container 를 손쉽게 작성할수 있다. 여기에서는 container 를 작성하기 위해서 배열(array)를 사용하고 있는데, STL에서 직접 제공하는 vector, deque 같은 것들을 사용하면 더욱더 유연하게 만들수 있다.


    4절. 결론

    STL의 최대 사용처는 적당히 효율적인 자료구조를 만들고 만들어진 자료구조에 알고리즘을 적용시키는 작업이다. STL은 이러한 작업을 매우 효율적으로 빠른시간에 끝낼수 있도록 도와준다. 머리에 생각한대로 구현이 가능하다고 보면 될것이다.

    보통 이런 자료구조와 알고리즘은 특별한 경우가 아니면 비슷비슷하다. 그러나 비슷함에도 불구하고 거의 매번 자료구조와 알고리즘을 다시 만들어야 할것이다. 자칫 필요 없는 단순 노가다에 시간을 빼앗길수가 있다. STL을 배우고 나면 자료구조와 알고리즘을 설계하고 코딩하는게 즐거워 질것이다.