'CPP'에 해당되는 글 5건

  1. 2015.12.23 STL 정렬
  2. 2015.12.23 STL 기본
  3. 2014.10.01 모듈 이 safeseh 이미지 에 대해 안전 하지 않습니다
  4. 2014.09.30 싱글톤 2
  5. 2014.09.29 싱글톤 패턴 (Singleton Pattern) 1

STL 정렬

CPP 2015. 12. 23. 12:50

※ 읽으시기 전에


- 제가 현재 숙지하고 있는 STL 내용은 대부분 레퍼런스를 참고하지 않고, 직접 이리저리 실험해가며 익힌 내용입니다. 글을 쓸 때는 레퍼런스를 이용하여 검증하지만, 그래도 이론적인 부분에서 제가 틀리게 작성할 수도 있습니다. 따라서 피드백은 언제나 환영입니다.


- 앞으로 소개드릴 자료구조, 함수 등은 반드시 직접 구현할 수 있어야 합니다. 100% 완벽하게 구현할 수 있어도 좋고, 핵심적인 부분만 구현할 수 있어도 좋습니다. 적어도 어떤 원리로 작동하는지는 알아야합니다. 원리를 모른 채 그냥 가져다 쓰는 건 언제 어떤 난관에 부딪칠지 모르기 때문에 상당히 위험합니다. 가져다 쓰는 건 구현이 가능한 이후입니다.




0. qsort 함수와 sort 함수와의 비교




 “qsort 함수가 있는데 sort 함수를 굳이 사용할 필요가 있나요?”


 


  네, 있습니다. 사실 이 질문은 C++를 아직 제대로 접하지 못하였기에 일어나는 질문입니다. C++ 에서는 여러 가지 자료구조가 내장 라이브러리 내에 선언되어 있습니다. C에서는 아직 이러한 자료구조들이 라이브러리에 포함되어 있지 않기 때문에 qsort 함수는 단순히 배열을 기반으로 설계한 함수입니다. 한편 sort 함수는 연속된 주소 값을 가지는 컨테이너라면 (RandomAccessIterator) 무엇이든 정렬이 가능합니다. 게다가 C++ 에서는 함수의 오버로딩 및 템플릿 화가 되어있어서 인자를 적게 받습니다. 즉, 사용이 용이합니다. 




  그리고 하나 더. sort 함수가 qsort 함수보다 수행 시간이 빠릅니다. 분할이 최악으로 일어날 때 부가적으로 처리하는 부분이 다른 것으로 알고 있습니다. (그런데 우리가 흔히 다루는 범위에서는 많아봐야 50ms 정도 차이나는 정도라 크게 신경 쓰시지 않으셔도 됩니다.)


 


  마지막으로, 참고용으로 stdlib.h 에 선언되어있는 qsort 함수의 원형을 제시하겠습니다. 바로 다음 장에 제시될 sort 함수의 원형과 비교해 보셨으면 합니다.


 


qsort(void *_Base, size_t_NumOfElements, size_t_SizeOfElements, int (*_PtFuncCompare)(const void *, const void *))


 


 


1. sort 함수의 원형 및 사용법


 


  sort 함수는 algorithm 헤더 파일의 std 네임스페이스 내부에 선언되어있습니다. 함수의 원형은 다음과 같습니다.


 


template<class RandomAccessIterator>


void sort(RandomAccessIterator first, RandomAccessIterator last);


 


template<class RandomAccessIterator, class Compare>


void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);


 


  두 번째 선언은 나중에 다시 설명하니 일단 넘어가고. 첫 번째 선언은 [first,last) 범위에 있는 원소를 오름차순으로 정렬합니다. 여기서 [first,last)란 first에 있는 원소는 포함하고, last에 있는 원소는 제외한다는 의미입니다.


  예를 들어 a[0], a[1], ... a[9]를 오름차순으로 정렬하고 싶으면, std::sort(a,a+10); 라 호출하면 됩니다. 전처리기에 using namespace std; 라 적어두면 std::를 생략할 수 있습니다.






ex)


 


--- Source --- 


#include<stdio.h>


#include<algorithm>


 


using namespace std;


 


int main() {


 


    int a[]={12,-6,7,903,88,-105,42,27,3,0};


    int size=10;


 


    sort(a,a+size);


    for(int i=0 ; i<size ; i++)


        printf("%d ", a[i]);


    printf("\n");


 


    return 0;


}


  


--- Output --- 


-105 -6 0 3 7 12 27 42 88 903


 


 


2. 내림차순 정렬은?


 


  내림차순 정렬을 위해서는 두 번째 선언을 사용합니다. 두 번째 선언에서 전달받는 인자 중 Compare comp는 비교 함수를 의미합니다. 여기에 functional 헤더 파일에 선언되어있는 greater<자료형>()을 전달하면 내림차순으로 정렬을 수행합니다. 이 비교 함수를 직접 구현하여 인자로 전달하여도 되지만, 여기서는 이미 내장 라이브러리에 선언되어있는 함수를 사용하도록 하겠습니다.


  참고로 less<자료형>()을 전달하면 오름차순으로 정렬을 수행합니다. 이는 첫 번째 선언과 동일한 의미를 가집니다.




ex)


 


--- Source --- 


#include<stdio.h>


#include<algorithm>


#include<functional>


 


using namespace std;


 


int main() {



    int a[]={12,-6,7,903,88,-105,42,27,3,0};


    int size=10;


 


    sort(a,a+size,greater<int>());


    for(int i=0 ; i<size ; i++)   


        printf("%d ", a[i]);


    printf("\n");


 


    return 0;


}




--- Output --- 


903 88 42 27 12 7 3 0 –6 –105




 


3. Compare 함수 전달

  앞서 내림차순 정렬을 위하여 sort 함수 세 번째 인자로 greater<자료형>()를 전달하였습니다. 여기서는 세 번째 인자로 전달되는 Compare 함수가 어떤 형식을 가지고 있어야 하는 지 설명하겠습니다.



  sort 함수의 인자로 전달되는 Compare 함수는, 정렬하고자 하는 자료형을 두 개 받은 후 정렬이 일어나는 방향으로 true를 반환해야 합니다. 무슨 소리냐고요? 예를 들어 int형 데이터를 오름차순으로 정렬하고 싶다면, int형 데이터를 두 개 받은 후 두 번째 인자가 더 크면 true, 더 작으면 false를 반환해야 한다는 것입니다. (여기서 같은 경우는 true나 false 무엇을 반환해도 상관없습니다.)



  부족한 설명을 대신할 예시를 전달하도록 하겠습니다...^^ 2장 내림차순 정렬 때 greater<자료형>() 대신 직접 Compare 함수를 구현하여 대체하였습니다.


 


ex)




--- Source ---


#include<stdio.h>


#include<algorithm>


 


using namespace std;


 


bool cmp(int a, int b) { return a>b; }


 


int main() {


 


    int a[]={12,-6,7,903,88,-105,42,27,3,0};


    int size=10;


 


    sort(a,a+size,cmp);


    for(int i=0 ; i<size ; i++)


        printf("%d ", a[i]);


    printf("\n");


 


    return 0;


}




--- Output ---


903 88 42 27 12 7 3 0 –6 –105






4. 구조체 정렬


 


  이 장에서는 구조체를 정렬하는 방법에 대하여 알아보도록 하겠습니다. 일단 구조체 A를 선언하고 sort 함수에 그대로 A를 인자로 전달하면 다음과 같은 컴파일 에러가 발생합니다.


 


  이항 ‘<’ : ‘A’이 (가) 이 연산자를 정의하지 않거나 미리 정의된 연산자에 허용되는 형식으로의 변환을 정의하지 않습니다.


 


  사실 몇 가지 에러가 더 발생하지만, 일단 위의 하나만 가져왔습니다. 무엇이 문제인지 대충 감이 잡히실 겁니다. C++에서 기본적으로 제공하는 char, short, int, ... 들은 < 연산이 모두 정의되어있습니다. 하지만 사용자 정의 자료형인 구조체는 < 연산이 기본적으로 정의되어 있지 않습니다.


 


  예를 들어봅시다. 만약 구조체 A가 다음과 같이 선언되어있고,


 


struct A {


    int d1,d2,d3;


};


 


  여기서 A 구조체인 X, Y에 대하여 X<Y 연산을 실행하였다고 합니다. 컴파일러는 d1, d2, d3 원소 중 어느 것을 비교해야 하는지 알 수가 없습니다. 따라서 컴파일 에러를 일으킵니다.


 


  즉, 구조체 정렬을 위해서는 두 구조체를 비교하는 근거를 제시한 Compare 함수를 인자로 전달하는 과정이 필요합니다. 아래 예시는 EDGE 구조체의 원소 중 w를 오름차순으로 정렬합니다.


 


--- Source ---


#include<stdio.h>


#include<algorithm>


 


using namespace std;


 


struct EDGE {


    int u,v;


    int w;


};


 


bool cmp_EDGE(EDGE a, EDGE b) { return a.w<b.w; }


 


int main() {


 


    EDGE A[]={{1,2,5},{1,4,10},{2,3,7},{3,4,2},{2,4,8},{3,1,1}};


    int size=6;


 


    sort(A,A+size,cmp_EDGE);


 


    for(int i=0 ; i<size ; i++)


        printf("u(%d) v(%d) w(%d)\n", A[i].u, A[i].v, A[i].w);


 


    return 0;


}


 


--- Output ---


u(3) v(1) w(1)


u(3) v(4) w(2)


u(1) v(2) w(5)


u(2) v(3) w(7)


u(2) v(4) w(8)


u(1) v(4) w(10)


 


 


※ 사실 위 소스는 크루스칼 알고리즘에서 간선 가중치를 오름차순으로 정렬하는 소스입니다.





 


  연산자 오버로딩을 알고 있다면 다른 방법도 가능합니다. 구조체를 정렬할 수 없는 이유는 < 연산자가 정의되어 있지 않다고 앞서 말씀드렸습니다. 다시 말해 < 연산자가 정의되어 있기만 하면 sort 함수가 구조체를 서로 비교할 수 있습니다.


 


  다음 예시는 바로 앞의 예시를 연산자 오버로딩을 이용하여 정렬을 수행합니다.


 


--- Source ---


#include<stdio.h>


#include<algorithm>


 


using namespace std;


 


struct EDGE {


    int u,v;


    int w;


    bool operator< (EDGE a) { return this->w<a.w; }


};


 


int main() {


 


    EDGE A[]={{1,2,5},{1,4,10},{2,3,7},{3,4,2},{2,4,8},{3,1,1}};


    int size=6;


 


    sort(A,A+size);


 


    for(int i=0 ; i<size ; i++)


        printf("u(%d) v(%d) w(%d)\n", A[i].u, A[i].v, A[i].w);


 


    return 0;


}


 


--- Output ---


u(3) v(1) w(1)


u(3) v(4) w(2)


u(1) v(2) w(5)


u(2) v(3) w(7)


u(2) v(4) w(8)


u(1) v(4) w(10)


 


 


  연산자 오버로딩이 객체지향적인 측면에서 조금 더 완성된 스타일이지만, 그렇다고 Compare 함수를 전달하는 방법이 좋지 않은 것은 아닙니다. 각자의 코딩 스타일대로 맞춰 쓰시면 될 듯합니다. (개인적으로는 Compare 함수 전달 방법을 더 많이 씁니다.)






5. stable_sort


 


  algorithm 헤더 파일에는 [first,last)를 정렬하는 함수로 sort 이외에도 stable_sort, partial_sort 가 있습니다. 이 장에서는 그 중 stable_sort에 대하여 설명하도록 하겠습니다.


 


  stable? 안정하다? stable_sort라고 했으니 안정한 정렬? 정렬이 안정하다는 것이 도대체 무슨 의미일까요? 다음 정의를 읽어봅시다.


 


Def) 안정 정렬(stable sort)


  동일한 키 값을 레코드들의 처음 순서가 정렬된 다음에도 그대로 유지되는 정렬.


 


  다시 말하여 같은 값을 가지는 구간은 정렬을 수행한 후에도 그 구간을 그대로 유지하고 있어야 한다는 이야기입니다.


  예를 들어 컨테이너에 {5,2,3,2,6,2} 가 들어있다고 합시다. 2의 개수가 세 개인데, 이들을 구분하기 위하여 두 번째, 세 번째 2는 !와 @를 붙여 구분하도록 하겠습니다. (!,@는 값과는 무관한 기호입니다.) 즉, 컨테이너에는 {5,2,3,2!,6,2@} 가 들어있는 것인데, 이 컨테이너를 오름차순으로 안정정렬하면 {2,2!,2@,3,5,6} 이 됩니다.


 


  만약에 stable_sort가 아닌 sort 함수로 위 컨테이너를 정렬하게 되면 어떻게 될까요? 그 때는 정렬의 안정성이 확보되지 않습니다. 따라서 세 개의 2가 어떤 순서로 정렬될지 알 수가 없습니다.


 


  정렬의 안정성을 확보하기 위하여 stable_sort는 퀵 정렬이 아닌 병합 정렬을 기반으로 합니다. 병합 정렬에서 두 집합을 합칠 때 처음부터 차례대로 원소를 읽어나가는 과정이 정렬의 안정성을 확보하는 근거가 됩니다.


  평균적인 성능으로는 퀵 정렬이 병합 정렬보다는 우수하지만, 최악의 경우에는 병합 정렬이 비교 정렬의 시간복잡도 하한에 더 근접하게 됩니다. (퀵 정렬은 O(n^2), 병합 정렬은 O(nlgn)) 그래서 정렬이 안정하다는 것을 시간복잡도의 변동 폭이 적은 것으로 말하는 분들이 있으신데, 틀린 말은 아니지만 정렬이 안정하다는 것은 같은 값은 순서가 유지된다는 의미로 받아들이는 게 더 적합한 듯 합니다.


 


  stable_sort 함수 원형은 다음과 같습니다. 사용법은 sort 함수의 사용법과 동일합니다.


 


template<class RandomAccessIterator>


void stable_sort(RandomAccessIterator first, RandomAccessIterator last);


 


template<class RandomAccessIterator, class Compare>


void stable_sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);


 


 


  참고로 하나 더 말씀드리자면, 비교를 기반으로 정렬하는 비교 정렬의 시간 복잡도는 최악의 경우 O(nlgn)을 밑돌 수 없다는 것이 증명되어있습니다. (차후 Math For CS 카테고리에 올릴 예정입니다.) 하지만 입력이 특수한 조건을 만족시키는 경우 이를 극복 가능한 정렬이 있습니다. 기수정렬이 대표적인데, 기수정렬이 위에서 설명한 안정 정렬을 이용하여 O(k) 정렬을 만들어냅니다. (k는 입력 값의 최대 자리 수, 만약 k>n이라면 기수 정렬의 이점이 사라짐.) 물론 안정한 정렬을 위하여 비교 정렬을 사용하지는 않습니다.

'CPP' 카테고리의 다른 글

STL 기본  (0) 2015.12.23
모듈 이 safeseh 이미지 에 대해 안전 하지 않습니다  (0) 2014.10.01
싱글톤 2  (0) 2014.09.30
싱글톤 패턴 (Singleton Pattern)  (1) 2014.09.29
Posted by wakira
,

STL 기본

CPP 2015. 12. 23. 12:48

이 주제는 C++ STL 의 기본에 가까운 듯 한데,

평소에 C++ 로 프로젝트가 진행되는 경우가 없어서 익숙하지가 않아,

쓸때마다 헷갈려서 ( 특히 object pointer 타입을 다룰 때... ) 정리차원에서 한 번 포스팅해 봄.


1. primitive 타입


먼저 가장 기본 형태. primitive type 의 vector 를 보자.

vector<int> v;


for (int i = 0; i < 20; i++)

    v.push_back(i);


random_shuffle(v.begin(), v.end());


for (int i = 0; i < 20; i++) cout << v[i] << " ";

cout << endl;


sort(v.begin(), v.end());


for (int i = 0; i < 20; i++) cout << v[i] << " ";

cout << endl;

(실행결과)

1 11 15 0 14 7 16 13 8 10 17 5 2 19 18 9 4 3 6 12 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 


위와 같이 오름차순으로 정렬이 된다.

기본적으로 std::sort 함수는 elements 비교에 '<' 연산자를 사용하기 때문이다.

그럼 내림차순으로 정렬을 하려면 어떻게 해야 하는가?

바로 compare 하는 함수를 지정해 주거나, compare object 를 지정해 주는 것이다.

이때 함수는 bool comp(int a, int b) 형태로 표현하면 된다.

중요한 점은 comp 함수의 return value 가 true 라면 a 가 b 보다 앞으로 간다는 것이다.

위의 코드에서 comp 함수를 추가하고 sort 를 아래와 같이 바꿔서 실행해 보면


// compare function

bool comp(int a, int b) {

    return (a > b);

}

// compare object

struct comp_object {

    bool operator() (int a, int b) {

        return (a > b);

    };

} comp_obj;


.

.

.


sort(v.begin(), v.end(), comp);

sort(v.begin(), v.end(), comp_obj);

(실행결과)

1 11 15 0 14 7 16 13 8 10 17 5 2 19 18 9 4 3 6 12 

19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0


위와같이 내림차순으로 정렬이 된다.

( compare object 에 대해서는 나도 잘 알지 못하기에 뒤의 class T, class T* 타입에 대한 내용에서는 패스한다.)




2. Class T 타입

이제 본격적으로 vector<class T> 타입의 정렬에 대해서 알아보자.

기본 사용법은 아래와 같다. 위에서 언급하였듯이 std::sort 는 elements 비교에 

'<' 연산자를 사용하므로 '<' 연산자를 오버로딩 해 주어야 한다.

class T {

public:

    int data;


    T (int a) { data = a; }

    bool operator<(const T &t) const {

        return (data < t.data);

    }

};



vector<T> v;


for (int i = 0; i < 20; i++) {

    v.push_back(T(i));

}


random_shuffle(v.begin(), v.end());


for (int i = 0; i < 20; i++) cout << v[i].data << " ";

cout << endl;


sort(v.begin(), v.end());


for (int i = 0; i < 20; i++) cout << v[i].data << " ";

cout << endl;

(실행결과)

17 2 15 14 0 3 1 9 5 10 8 7 18 19 11 16 6 13 12 4 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 


'<' 연산자는 sort 뿐만 아니라 다른 코드에서도 사용할 수 있으므로,

'<' 연산결과와 다른 순서로 sorting 을 하고 싶다면,

compare 함수를 직접 지정해 주면 된다.

class T {

public:

    int data;


    T (int a) { data = a; }

    bool operator<(const T &t) const {

        return (data < t.data);

    }


    static bool comp(const T &t1, const T &t2) {

        return (t1.data > t2.data);

    }

};


.

.

.


sort(v.begin(), v.end(), T::comp);


물론 comp 함수가 class T 의 멤버함수일 필요는 없다.

( 하지만 더 깔끔해 보이지 않은가? )




3. Class T* 타입

(중요) T* 타입은 class 의 '<' 연산자 오버로딩을 하여도 제대로 동작하지 않는다!!!

게다가 아래와 같은 코드가 함께 있다면,

bool operator<(const T &t) const { ... }

빌드 에러도 발생하지 않는다.

가령 위의 코드에서 T 의 멤버함수에 아래와 같이 연산자 오버로딩을 하고,

bool operator<(const T *t) const {

    return(data < t->data);

}

vector<T*> 에 대해서 std::sort 수행하여도 저 함수는 호출되지 않는다.

vector<T*> 에 대하여 std::sort 를 하기 위해서는,

T* 에 대한 compare fuction 이나 compare object 를 지정해 주어야 한다.


class T {

public:

    int data;


    T (int a) { data = a; }

    static bool comp(const T *t1, const T *t2) {

        return (t1->data < t2->data);

    }

};

.

.

.

sort(v.begin(), v.end(), T::comp);



쓰다보니 가장 중요한 내용이 제일 마지막에 있게 되었는데, 잘 기억해 두자.

'CPP' 카테고리의 다른 글

STL 정렬  (0) 2015.12.23
모듈 이 safeseh 이미지 에 대해 안전 하지 않습니다  (0) 2014.10.01
싱글톤 2  (0) 2014.09.30
싱글톤 패턴 (Singleton Pattern)  (1) 2014.09.29
Posted by wakira
,

추가

* SAFESEH ( Structured Exception Handling )

- 윈도우 상에서 예외처리를 하는 기법

- 메모리에 대한 손상이 발생하거나 시스템이 예기치 않게 종료되는 이벤트 발생시 프로그램에 대한 예외처리 담당

- 외부 공격자가 프로그램에 대해 예외처리 레코드를 덮어쓰게 되면 이 예외를 탐지하여 프로그램을 종료시킴

 

 

 

* 옵션 On/Off

  프로젝트 속성 -> 링커 -> 고급 -> 이미지에 안전한 예외처리기 포함  ( 예/아니오 )

  혹은 명령줄 에서  /SAFESEH:NO 입력


해결법

프로젝트 속성->구성 속성->링커->명령줄

 

/safeseh:no

'CPP' 카테고리의 다른 글

STL 정렬  (0) 2015.12.23
STL 기본  (0) 2015.12.23
싱글톤 2  (0) 2014.09.30
싱글톤 패턴 (Singleton Pattern)  (1) 2014.09.29
Posted by wakira
,

싱글톤 2

CPP 2014. 9. 30. 09:24

이 문서에서는 Singleton 디자인 패턴에 대한 몇가지 이슈를 소개합니다. Singleton 패턴은 클래스의 인스턴스가 단 하나만 생성되도록 제한하기 위한 패턴입니다. “GoF의 디자인패턴”에서는 “클래스가 단 하나의 인스턴스만 가지는 것을 보장하고, 전역에서 그 인스턴스에 접근할 수 있도록 지원한다.”라고 설명합니다.

Singleton 패턴은 보기보다 간단하지 않고 구현에 대한 많은 논의가 존재합니다. 그 중에 C++11을 기반으로 한 몇몇의 구현에 대해 정리하고자 합니다.

기본 아이디어는 Singleton 클래스를 private인 static 인스턴스로 구현하고 생성자 역시 private이며 인터페이스 메서드가 static 인스턴스를 리턴하는 것으로 하겠습니다.

Version1

대부분의 일반적인 접근 방식은 아래와 같을 것입니다.

class simpleSingleton
{
    simpleSingleton();
 
 
    static simpleSingleton* _pInstance;
public:
    ~simpleSingleton() {
    }
 
    static simpleSingleton* getInstance() {
        if(!_pInstance) {
            _pInstance = new simpleSingleton();
        }
 
        return _pInstance;
    }
 
    void demo() {   std::cout << "simple singleton # next - your code ..." << std::endl;    }
};
 
simpleSingleton* simpleSingleton::_pInstance = nullptr;

아쉽게도 이러한 방법은 여러가지 문제점을 안고 있습니다. 기본 생성자가 private이지만 복사 생성자가 할당 연산자가 컴파일 타임에 정의되기 때문에 다음과 같은 동작이 성립하게 됩니다.

// Version 1
simpleSingleton * p = simpleSingleton::getInstance();   // cache instance pointer
p->demo();
 
// Version 2
simpleSingleton::getInstance()->demo();
 
simpleSingleton ob2(*p);          // copy constructor
ob2.demo();
 
simpleSingleton ob3 = ob2;        // copy constructor
ob2.demo();

따라서 복사 생성자와 할당 연산자도 명시적으로 private를 정의해 줘야 합니다.

Version2

Scott Meyers의 Effective C++에서 위의 버전을 약간 개선하고 getInstance()함수가 포인터가 아닌 레퍼런스를 리턴하도록 수정한 것을 소개했습니다. 따라서 포인터를 최종적으로 제거해야 하는 이슈가 사라졌죠.

이 방식의 한가지 장점은 함수 내부에서 static 객체를 초기화 하는 것은 함수를 처음 호출하는 시점이라는 것입니다.

class otherSingleton
{
    static otherSingleton * pInstance;
 
    otherSingleton ();
 
    otherSingleton(const otherSingleton& rs) {
            pInstance = rs.pInstance;
    }
 
    otherSingleton& operator = (const otherSingleton& rs) {
           if (this != &rs) {
           pInstance = rs.pInstance;
           }
 
           return *this;
    }   
 
        ~otherSingleton ();
 
  public:
 
    static otherSingleton& getInstance()
    { 
        static otherSingleton theInstance;
        pInstance = &theInstance;
 
         return *pInstance;
    }
 
    void demo() {   std::cout << "other singleton # next - your code ..." << std::endl;     }
};
 
otherSingleton * otherSingleton::pInstance = nullptr;

소멸자를 private로 두어 클라이언트에서 객체의 포인터를 이용해 실수로 delete를 수행하는 것을 막았습니다. 따라서 객체의 복제가 생성되는 것을 하용하지 않습니다.

otherSingleton ob = *p;
ob.demo();

error C2248: otherSingleton::otherSingleton ' : cannot access private member declared in class 'otherSingleton'
error C2248: 'otherSingleton::~otherSingleton' : cannot access private member declared in class 'otherSingleton'

하지만 다음과 같은 동작이 가능합니다.

// Version 1
otherSingleton *p = & otherSingleton::getInstance();    // cache instance pointer
p->demo();
// Version 2    
otherSingleton::getInstance().demo();

이런 구현 방식은 쓰레드 안정성을 보장하지 못합니다.

Multi-threaded environment

위의 두 방식은 싱글쓰레드 어플리케이션에서는 괜찮을 수 있지만 멀티쓰레드 환경을 고려하기에는 어렵습니다. Raymond Chen은 왜 C++ static이 Thread-safe하지 않은지를 설명합니다. 공유된 전역 리소스은 레이스컨디션이나 여러가지 쓰레딩 이슈를 유발합니다. 따라서 위의 Singleton 객체는 이러한 이슈에 버틸수가 없습니다. 다음과 같은 상황이 멀티쓰레드 환경에서 일어난다고 생각해 봅시다.

static simpleSingleton* getInstance()
{
     if(!pInstance)                              // 1
     {
         pInstance = new simpleSingleton();      // 2
     }
 
     return pInstance;                           // 3
}

첫번째 쓰레드에서 getInstance()를 호출하면 pInstance가 null입니다. 쓰레드는 (2)에 도달하고 new를 호출할 준비를 하고 있습니다. 바로 그 때!! OS 스케쥴러가 인터럽트를 걸고 제어권을 다른 쓰레드에 넘겨버립니다. 두번째 쓰레드는 같은 과정을 수행하여 new를 호출하고 pInstance에 할당하여 그 포인터를 가져 갑니다. 다시 첫번째 쓰레드에게 기회가 돌아오면 아까 준비하고 있던 new를 호출하고 또 다시 pInstance에 할당하여 그 포인터를 가져 갑니다. 이제 우리는 하나가 아닌 두개의 Singleton 객체를 가지게 되었고, 심지어 메모리릭을 발생시킬 겁니다. 두 쓰레드는 서로 다른 인스턴스를 가지게 되었네요.

이러한 상황을 개선하기 위해서 C++11에 추가된 쓰레드 락 메커니즘을 사용해 봅시다. Scott Meyers의 구현은 아래와 같습니다.

static otherSingleton& getInstance()
{ 
      std::lock_guard<std::mutex> lock(_mutex);   
 
      static otherSingleton theInstance;
      pInstance = &theInstance;
 
    return *pInstance;
}

std::lock_guard는 생성자에서 뮤텍스에 락을 걸고 소멸자에서 락을 해제 합니다. _mutex가 락이 걸려 있을 때는 다른 쓰레드가 접근할 수 없죠. 하짐나 이 구현방법은 getInstance를 호출할 때 마다 불필요한 동기화 관련 오버헤드를 감수해야 합니다. Singleton객체를 필요로 할 때마다 락을 걸고 있는데 실상 락은 최초로 pInstance를 초기화 할때만 필요합니다. getInstance()가 몇번 호출되든지 락은 최초 한번만 동작해 주는게 필요합니다.

C++은 쓰레드 관련 표준 지원이 없었기 때문에 Singleton이 확실하게 쓰레드 안정성 보장하는 것은 간단한 문제가 아니었습니다.

Singleton이 쓰레드 안정성을 보장하기 위해서는 double-checked locking(DCLP) 패턴을 적용해야 합니다. 이 패턴은 동기화 코드에 진입하기 전에 조건 체크를 한번 더 수행합니다. 그래서 Version1을 임시객체를 사용하도록 재작성했습니다.

static simpleSingleton* getInstance() 
{
    if (!pInstance) 
    {
        std::lock_guard<std::mutex> lock(_mutex); 
 
        if (!pInstance) 
        {
            simpleSingleton * temp = new simpleSingleton;
            pInstance = temp;
        }
    }
 
    return pInstance;
}

이 패턴은 락을 걸기 전에 pInstance가 null인지를 체크하고 null이라면 락을 걸어준 후에 다시한번 pInstance가 null인지를 확인합니다. 두번째 테스트는 여러 쓰레드에서 pInstance를 초기화 함과 동시에 null체크를 수행하는 레이스컨디션 때문에 필요합니다.

이론적으로 이 패턴은 옳지만 실상 언제나 제대로 동작하지는 않습니다. 특히 멀티프로세서 환경에서 말이죠.

하나의 프로세서가 메모리에 접근하여 쓰기 동작의 재정렬을 수행하는 동안 다른 프로세서에서 같은 메모리를 참조하는 동작은 제대로 수행되지 않습니다. 이 말은 Singleton객체가 완전히 초기화 되기 이전에 pInstance가 할당되는 동작이 일어날 수 있다는 것입니다.

첫번째 getInstance()의 호출 이후에 pointer를 이용한 구현은 메모리 릭을 위한 포인터를 필요로 하게 됩니다.

Version3 - Singleton with smart pointers

C++11 이전에는 C++ 표준에 쓰레딩 모델을 지원하지 않아 POSIX나 OS API 같은 외부 API를 사용해야 했습니다. 하지만 C++11에서 쓰레딩 모델을 표준으로 지원하기 시작했습니다. 불행히도 Visual Studio 2010에서의 C++ 표준은 쓰레드를 완전히 지원하지 못하는 버전입니다. 하지만 Visual Studio 2012 부터는 완전히 지원하기 시작했습니다.

class smartSingleton 
{
  private:
    static std::mutex              _mutex;
    static std::weak_ptr<smartSingleton>  thisObjPtr;
 
    smartSingleton();
    smartSingleton(const smartSingleton& rs);
    smartSingleton& operator = (const smartSingleton& rs);
 
 
  public:
     ~smartSingleton();
 
    static std::shared_ptr<smartSingleton>& getInstance()
    { 
        static std::shared_ptr<smartSingleton> instance = thisObjPtr.lock();
 
        if (!instance)                                  
        {
            std::lock_guard<std::mutex> lock(_mutex); 
 
            if (!instance)  
                     {
                instance.reset(new smartSingleton());
                thisObjPtr = instance;
            }
        }
 
        return instance;
    }
 
    void demo() {   std::cout << "smart pointers # next - your code ..." << std::endl;  }
};
 
std::weak_ptr< smartSingleton > smartSingleton:: thisObjPtr = nullptr;

알다시피 C++ 클래스의 기본 접근자는 private 입니다. 따라서 우리의 기본 생성자 역시 private 입니다. 결국 우리는 Singleton 인스턴를 쓰레드 걱정없이 사용할 수 있게 된 듯 합니다.

// Version 1
std::shared_ptr< smartSingleton > p = smartSingleton::getInstance();  // cache instance pointer
p->demo();
 
// Version 2
std::weak_ptr< smartSingleton > pw = smartSingleton::getInstance();   // cache instance pointer
pw.lock()->demo();
 
// Version 3
smartSingleton::getInstance()->demo();

그리고 메모리 릭도 발생하지 않네요. 여러 쓰레드는 서로다은 std::shared_ptr를 통해서 일제히 읽기 쓰기를 수행할 수 있고, 객체가 복사되었을 때에도 객체의 소유 정보를 공유합니다. 하지만 이렇게 DCLP을 이용한 구현에도 여전히 쓰레드 안정성이 보장되지 않습니다.

Version4 - Thread safe singleton C++11

쓰레드 안정성이 보장되는 구현을 위해서는 멀티쓰레드 환경에서 단한번 락을 걸어 생성하는 것을 보장하는 것이 필요합니다.

다행히 C++11에서 우리를 도울 새로운 2가지 요소가 추가되었습니다. std::call_once와 std::once_flag 입니다. 이것을 이용하면 기본 컴파일러는 우리의 Singleton의 쓰레드 안정성과 메모리릭을 보장해 주게 됩니다.

std::once_flag의 인스턴스는 std::call_once와 함께 사용되며 특정 함수가 딱 한번 호출되는 것을 보장해 줍니다 .심지어 여러 쓰레드가 동시에 호출하더라도 한번만 호출합니다. 
std::once_flag는 복사생성자, 할당연산자, 이동생성자, 이동연산자를 지원하지 않습니다.

이것이 C++11에서 쓰레드 안정성을 보장하는 구현에 대한 제안입니다.

class safeSingleton 
{
    static std::shared_ptr< safeSingleton > instance_;
    static std::once_flag                   only_one;
 
    safeSingleton(int id) {
        std::cout << "safeSingleton::Singleton()" << id << std::endl;
    }
 
    safeSingleton(const safeSingleton& rs) {
        instance_  = rs.instance_;
    }
 
    safeSingleton& operator = (const safeSingleton& rs) 
    {
        if (this != &rs) {
            instance_  = rs.instance_;
        }
 
        return *this;
    }   
 
public:
    ~safeSingleton() {
        std::cout <<  "Singleton::~Singleton" << std::endl; 
    }
 
    static safeSingleton & getInstance( int id ) 
    {
        std::call_once( safeSingleton::only_one, 
                [] (int idx)
                { 
                   safeSingleton::instance_.reset( new safeSingleton(idx) ); 
 
                   std::cout << "safeSingleton::create_singleton_() | thread id " + idx << std::endl;
                 }
                , id );
 
        return *safeSingleton::instance_;
    }
 
    void demo(int id) { std::cout << "demo stuff from thread id " << id << std::endl;     }
};
 
std::once_flag                   safeSingleton::only_one;
std::shared_ptr< safeSingleton >  safeSingleton::instance_ = nullptr;

getInstance()함수에 전달되는 파라메터는 테스트를 위한 코드일 뿐이니 무시해 주세요. 보시는것 처럼 일반 메서드 대신 람다식을 사용했습니다. 아래는 safeSingleton 클래스를 테스트해본 코드 입니다.

std::vector< std::thread > v;
int num = 20;
 
for( int n = 0; n < num; ++n ) {
    v.push_back( std::thread( []( int id )
                              {
                             safeSingleton::getInstance( id ).demo( id );
                                }
                           , n ) );
 }
 
std::for_each( v.begin(), v.end(), std::mem_fn( &std::thread::join ) );
 
// Version 1
std::shared_ptr<smartSingleton> p = smartSingleton::getInstance(1);   // cache instance pointer
p->demo("demo 1");
 
// Version 2
std::weak_ptr<smartSingleton> pw = smartSingleton::getInstance(2);    // cache instance pointer
pw.lock()->demo(2);
 
// Version 3
smartSingleton::getInstance(3)->demo(3);

20개의 쓰레드를 만들고 동시에 getInstance() 함수에 접근하였습니다. 딱 하나의 쓰레드에서만 instance를 생성하는데 성공하였습니다.


http://milennium9.godohosting.com/doku.php?id=cpp:tiptricks:multithread_singleton


'CPP' 카테고리의 다른 글

STL 정렬  (0) 2015.12.23
STL 기본  (0) 2015.12.23
모듈 이 safeseh 이미지 에 대해 안전 하지 않습니다  (0) 2014.10.01
싱글톤 패턴 (Singleton Pattern)  (1) 2014.09.29
Posted by wakira
,

무분별한 객체생성을 방지하고, 1개의 객체만 생성하여 이용하는 프로그램 코딩에 유용하게 적용할 수 있다.


일반적인 싱글톤 패턴의 코드는 아래와 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Singleton
{
private:
  Singleton() {}
  ~Singleton() {}
  Singleton(const Singleton& s) {}
 
public:
  static Singleton* GetInstance()
  {
    if (m_pInstance == NULL)
    {
      m_pInstance = new Singleton;
    }
 
    return m_pInstance;
  }
 
private:
  static Singleton* m_pInstance;
};
 
//------------------------------------------------------------------
// Main
int _tmain(int argc, _TCHAR* argv[])
{
  Singleton* pSingleton = Singleton::GetInstance();
 
  return 0;
}


문제가 없는듯 보이는데 그러나 이 패턴에는 문제점이 존재한다. 바로 new로 생성한 객체의 소멸을 보장받지 못하는 것이다. (리소스 누수현상 발생)

이러한 누수현상에 대처하기 위해서 동적 생성이 아닌 정적 생성 사용을 예로 들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Singleton
{
private:
  Singleton() {}
  ~Singleton() {}
  Singleton(const Singleton& s) {}
 
public:
  static Singleton* GetInstance()
  {
    return &m_pInstance;
  }
 
private:
  static Singleton m_pInstance;
};
 
//------------------------------------------------------------------
// Main
int _tmain(int argc, _TCHAR* argv[])
{
  Singleton* pSingleton = Singleton::GetInstance();
 
  return 0;
}

정적 객체를 생성하여 프로그램 종료시 객체 반환할 필요가 없어졌다.


처음으로 돌아가서 동적 생성을 통한 싱글턴 패턴 인스턴스 소멸은 어떻게 가능할 수 있는지 확인해보도록 하자.

atexit() 함수에 대하여

atexit 함수는 프로그램 종료시에 (main함수 종료 직전) 실행되며, 인자는 함수 포인터를 가진다.

int atexit(

   void (__cdecl *func )( void )

);

이 정의에서 알수 있듯이 atexit 함수는 인수를 취할 수 없고, 리턴값도 없는 void func(void)형이어야 한다. 또한 이 함수는 32개까지 등록될 수 있으며 최후로 등록된 함수가 가장 먼저 실행된다.

atexit함수를 이용하면 프로그램 종료 직전에 내가 원하는 작업을 할 수 있는 기능을 만들어 줄 수가 있게 된다. 


아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "stdafx.h"
 
class Singleton
{
private:
  Singleton() {}
  ~Singleton() {}
  Singleton(const Singleton& s) {}
 
private:
  static void destroy() { delete m_pInstance; }  // 객체 소멸
 
public:
  static Singleton* GetInstance()
  {
    if (m_pInstance == NULL)
    {
      m_pInstance = new Singleton();
      atexit(destroy);    // 프로그램 종료 직전호출
    }
 
    return m_pInstance;
  }
 
private:
  static Singleton* m_pInstance;
};
 
Singleton* Singleton::m_pInstance = NULL;
 
//------------------------------------------------------------------
// Main
int _tmain(int argc, _TCHAR* argv[])
{
  Singleton* pSingleton = Singleton::GetInstance();
 
  return 0;
}


자 위 코드는 동적 객체를 생성하고 atexit()함수를 이용하여 main()함수 종료 전에 객체를 delete하도록 하였다. 이렇게 하면 객체 생성 및 해제를 아주 깔끔하게 해결 할 수 있다.


템플릿을 이용한 싱글턴 패턴 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "stdafx.h"
 
//------------------------------------------------------------------
// template singleton pattern
template<typename T>
class Singleton
{
protected:
  Singleton() {}
  virtual ~Singleton() {}
  Singleton(const Singleton& s) {}
 
private:
  static void destroy() { delete m_pInstance; }  // 객체 소멸
 
public:
  static T* GetInstance()
  {
    if (m_pInstance == NULL)
    {
      m_pInstance = new T();
      atexit(destroy);    // 프로그램 종료 직전호출
    }
 
    return m_pInstance;
  }
 
private:
  static T* m_pInstance;
};
 
template <typename T> T* Singleton <t>::m_pInstance;
 
//------------------------------------------------------------------
// test code
 
class 테스트 : public Singleton<테스트>
{
public:
  void 싱글턴_테스트() { cout << "템플릿 싱글턴 테스트 합니다." << endl; }
};
 
//------------------------------------------------------------------
// Main
int _tmain(int argc, _TCHAR* argv[])
{
  테스트::GetInstance()->싱글턴_테스트();
 
  return 0;
}




멀티 스레드 기반에서 싱글턴 패턴 구현 (volatile 활용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template<typename T>
class CSingleton : public CMultiThreadSync<T>
{
protected:
  CSingleton() {}
  virtual ~CSingleton() {}
  CSingleton(const CSingleton& s) {}
 
private:
  static void destroy() { delete m_pInstance; }  // 객체 소멸
 
public:
  static T* GetInstance()
  {
    if (m_pInstance == NULL)
    {
      CThreadSync cs;
 
      m_pInstance = new T();
      atexit(destroy);    // 프로그램 종료 직전호출
    }
 
    return m_pInstance;
  }
 
private:
  static T* volatile m_pInstance;
};
 
template <typename T> T* volatile CSingleton <T>::m_pInstance;




스마트 포인터 shared_prt<> 을 활용한 싱글턴 패턴

스마트 포인터와 shared_ptr 이란?

  • 스마트 포인터 : heap 메모리에 new, malloc으로 할당된 경우 자동으로 free, delete(소멸) 해주는 똑똑한 포인터 (auto_ptr, shared_ptr, weak_ptr 등이 있다)
  • shared_ptr : 참조 횟수(reference counting)에 따른 메모리 할당 해제를 수행한다. 즉, 최초 객체를 할당시 실질적으로 메모리에서 할당받고, 그 다음부터는 객체 복사시 실제 복사는 이루어지지 않고 카운팅을 올려서 관리하는 기법이다. 또한 카운팅이 0가 되었을 경우 메모리를 자동 해제하게 된다.


shared_ptr<자료형> 이름(사이즈);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
class CSingleton
{
public:
  static T* GetInstance()
  {
    std::call_once(m_onceFlag, [] {
      m_pInstance.reset(new T);
    });
 
    return m_pInstance.get();
  }
 
private:
  static std::shared_ptr<T> m_pInstance;
  static std::once_flag m_onceFlag;
};
template<typename T> std::shared_ptr<T> CSingleton<T>::m_pInstance = NULL;
template<typename T> std::once_flag CSingleton<T>::m_onceFlag;


우리는 위에서 atexit() 함수를 활용하여 소멸자 역할을 할 수 있는 함수를 프로그램 종료직전에 명시적으로 호출하도록 하였다. 하지만 이 기능은 뭔가 꺼림찍하고 깔끔해보이지 않는다. 그래서 shared_ptr 스마트포인터를 활용하여 새로운 싱글턴 패턴을 구현하였다.

'CPP' 카테고리의 다른 글

STL 정렬  (0) 2015.12.23
STL 기본  (0) 2015.12.23
모듈 이 safeseh 이미지 에 대해 안전 하지 않습니다  (0) 2014.10.01
싱글톤 2  (0) 2014.09.30
Posted by wakira
,