'string switch'에 해당되는 글 1건

  1. 2011.04.24 C/C++ switch case 에 문자열 사용하기 1


해당 방식은 내가 만든 것이 아니라 이미 숙련된 프로그래머라면 대부분 쓰고 있을(또 만들 수 있는) 방법이다.

그 동안 이 방식보다는 다른 방법을 이용했었는데, 이번에 라이브러리화시키면서 끝내 만들게 되었으므로 하는 김에

조금 더 시간을 투자하여 많은 분들(특히 프로그래머 지망생들)에게 소개하고자 한다.

이 글을 읽기 위해서는 꽤 폭넓은 기반 지식이 필요하다(깊을 필요는 없다).

가능한한 모든 연관된 것을 설명하겠지만 링크나 검색 등으로 때우는 경우도 있을 것이다.




switch(sz)
{
  case "Name":
    ...
    break;
  case "Age":
    ...
    break;
}


위와 같은 구문이 C나 C++ 상에서 돌아가지 않는다는 것은 이미 잘들 알고 있는 사실이다.
(당연하죠! ... 때문에 컴파일 오류가 나잖아요? 라고 반문하는 사람은 없길 바란다)


switch 명령은 case가 많을수록 이론상 if문과의 성능 차이가 기하급수로 벌어지게 되어 있다.

이론상인 이유는 if - else if 열거식의 경우에도 만약 비교하는 대상 중 어느 한쪽이 상수라면 현대의 똑똑한 컴파일러가

자동으로 점핑 테이블을 만들어 마치 switch - case처럼 동작하기 때문이다.

상수 값(예제) 호출될 주소 (상대값)
2 0x0000
4 0x0016
6 0x0048
8 0x0052

※ 상수 값에 따라 IP 는 마치 함수호출처럼 자신의 주소를 향해 펄쩍 뛰어간다. break 구문을 만나면 switch 가 끝나는 블럭으로 또 한 번 점프한다. break 나 continue 는 goto 구문의 다른 (특수한 용도로 한정된) 이름이다.




그런데 상수가 아닐 경우는 점핑 테이블 생성이 불가능해진다. 아니, 불가능하다기보다는 우리가 원하는 그 성능이 나올 수가 없다.

점핑 테이블을 쓰는 것은 시간이 오래 걸리는 테이블 생성 작업을 1회 한 후(2011. 05. 31. 수정되었음) 한 번의 비교로 원하는 곳으로 찾아가 작업하겠다는 성능을 기대하는 것인데

변수가 case에 들어가게 되면 일반적인 if - else if 로밖에 구현이 안되는 것이다.

EIP = EIP + jmptable[nIndex];


하지만 변수가 case 에 가능하다고 하면 대체 jmptable 은 얼마나 커져야할까. 게다가 변수가 상수가 아닐 경우에는

nIndex 값 자체가 산출되지 않아 저런 식으로는 불가능하고 연결리스트 따위를 동원해야하니 속도 성능 같은 건 나락으로

떨어지는 것이다.




그래서 보통 빨리 짜야하거나 몇 가지 안되는 케이스를 가진 문자열 비교는 고전적으로 if - else if 로 행해진다

if(strcmp(sz, "Name") == 0)
{
}
else if(strcmp(sz, "Age") == 0)
{
}
else ...

이런 연쇄 else if 구문을 볼 때마다 속이 메스껍고 피로함을 느낀다면, 반갑다. 나도 그렇다.

게다가 더 큰 문제는 else if 중첩에는 한계가 있다는 것이다(error code C1061 참고)
(물론 정확히는 else if 중첩만이 문제가 아니지만, 어쨌든 많이 쓸 수가 없다)


그래서 이것을 개선한다면, 전통적으로 map이나 hash_map 을 사용해왔다.


static map< string, int > m;
void Init()
{
  m["Name"] = ENUM_NAME;
  m["Age"] = ENUM_AGE;
}

void func()
{
  switch(m[sz])
  {
    case ENUM_NAME:
      ...
      break;
    case ENUM_AGE:
      ...
      break;
  }
}



오, 오오... 깔끔하게 스위치 전환에도 성공했고 성능상의 이점도 가져왔다.

아 그런데... 뭐 좀 부족함이 느껴지지 않는가?

저런 문자열 비교 루틴이 수십 군데서 사용된다고 생각해보자(물론 다들 문자열 목록이 다른).

그런 경우가 얼마나 있겠냐고? 단순히 스크립트 데이터를 해석하는 코드만 해도 저런 것들이 부지기수다.


그래, 난 근성가이야. 하면서 그 수십 군데의 루틴을 모두 저렇게 열거형 상수를 선언하고 맵으로 고친다...

못할 건 없다. 초기화 함수 하나 만들고 변수 하나 선언하고 열거형 쯤 만들면 되지.

하지만 저런 부분들은 노가다 작업이다. 아무나 해도 될 뿐더러 저런 작업을 하고 있으면 굉장한 손해를 본다.

성능상으로야 가장 퍼펙트하지만(map 대신 hash_map을 쓴다면, 비록 표준은 아니지만 더욱 성능으로는 우위에 설 수 있을 것이다)

코드의 유지보수 관리에는 개떡이 되는 것이다.

매번 문자열 토큰(대개 태그라고도 하는데) 하나 추가될 때마다 상수도 추가해야하고 초기화에 라인도 추가해야하고

뭔가 작업할 곳이 최소한 세 군데가 생기는 것이다(열거형 상수쪽, 초기화쪽, switch 안)



그래서 생각하기를, 그냥 case 하나 달랑 추가하면 그 모든 걸 자동으로 해주는 신통방통한 녀석 없을까? 다.

완전 베스트 아닌가? 소개한다.


아래 소개된 코드는 앞서 이야기한대로, 내가 최초 사용한 기법은 아니지만, 작성은 스스로 했다(내 입맛에 맞게).

하지만 이해하기 쉽도록 몇 부분을 고쳤으며 이대로 활용하기엔 조금 불안한 부분이 있음을 미리 고백한다.
(뭐, 실무용으론 조금 부족하지만 예제용으론 그냥 써도 된다)


#define STR_SWITCH_BEGIN(key) \
{ \
  static stdext::hash_map< string, int > s_hm; \
  static bool s_bInit = false; \
  bool bLoop = true; \
  while(bLoop) \
  { \
    int nKey = -1; \
    if(s_bInit) { nKey=s_hm[key]; bLoop = false; } \
    switch(nKey) \
    { \
      case -1: {

#define CASE(token)  } case __LINE__: if(!s_bInit) s_hm[token] = __LINE__; else {
#define DEFAULT()    } case 0: default: if(s_bInit) {

#define STR_SWITCH_END()   \
      }      \
    }       \
    if(!s_bInit) s_bInit=true; \
  }        \
}



실제 사용은 다음과 같이 한다

STR_SWITCH_BEGIN(sz)
{
  CASE("Name")
    ...
    break;
  CASE("Age")
    ...
    break;
}
STR_SWITCH_END()


자, 이제 우리 입맛에 맞는 것들이 준비되었다. 앞으로 case가 추가된다 하더라도 단지 저곳에만 작업해주면 된다.

그럼 나머지 부분들은 모두 자동화되어 처리된다.

그런데 여기서 그냥 납득하지 않고 의문을 갖는 분들이 있다. 현명한 자세다. 세상에 공짜는 없는고로, 저런 게 그냥 됐으면

진작에 C++ 에서 표준으로 제공했을 것이다.

map(또는 hash_map)을 사용한 switch 보다 손해보는 점은 case 구문마다 if가 들어간다는 점과 위에서 미리 사용한

변수들이 자칫 기존 코드와 겹칠 수 있다는 점, 마지막에 STR_SWITCH_END 가 들어가 혼동된다는 점 등이다.

하지만 이 정도는 감수할 수 있을 정도에 불과하며, 사후 코드 관리 측면에서는 월등한 이점을 가져다준다.



마지막으로, 코드만 봐도 쉽게 이해할 수 있겠지만 위의 것을 사용할 때 놓치기 쉬운 몇 가지 주의점에 대해 언급하고자 한다.




1. 최초 1회에 한하여 초기화 과정을 수행하는데, 따라서 처음 1번의 비교는 제법 시간이 걸린다(모든 요소에 대해 일단 다 돌고 나서 원래 비교하려고 했던 값을 비교하므로). 그러니 만약 1회 비교하고 끝나는 루틴 같은 곳에는 저거 쓰지 말기 바란다.


2. hash_map 은 현재 C++ 표준이 아니다. 물론 그럴 확률은 지극히 낮지만, 언제 가서 코드가 메롱해질지 장담할 수 없다. 불안하다면 그냥 map 을 써도 좋다. map 은 표준인 만큼 견고함에 있어서는 해시보다 월등하다. 물론 비교 성능은 해시보다 좀 떨어진다.
(그래서 나도 해시맵을 탐탁찮게 생각하는 사람 중 하나이며, 해싱이라는 스킬 자체가 과연 키 해싱에 대해 무결성이 가능한지에 대해서도 회의적이므로 늘 불안불안하다)


3. VS 상에서 컴파일할 경우 릴리즈모드는 상관없으나 디버그모드에서는 컴파일 옵션을 바꿔주지 않으면 컴파일이 되지 않는다. 저 부분이 무슨 역할을 하는지는 검색하면 금방 나오니 활용하길 바란다.


원래 디버그 모드는 Program Database for Edit & Continue 옵션으로 되어 있는데

이것을 Program Database 로 바꿔주어야 한다(릴리즈 모드 옵션이다)


__LINE__ 이라는 매크로 때문인데 이것이 /ZI 옵션 상에서는 상수가 아니라 변수로 제어되기 때문에

상수로 코드에 박히는 /Zi 옵션이 필요한 것이다.
(case 에는 상수만 쓸 수 있다는 것을 다시 한 번 상기하자)


4. 실제 돌려보면 알겠지만, case 부분에 변수가 사용 가능하다! 하지만 컴파일만 되는 것일 뿐, 수행 결과는 첫 1회 초기화 때의 값에 따라 switching 되어 나온다. 너무 들뜨지 않길 바란다. 왜 그런지는 소스를 보면 잘 알 수 있을 거라 생각된다.



5. 혹시나 해서 첨언한다. 위의 예제 코드와 내가 만든 코드 사이의 차이는 언어 코드(MBCS / UNICODE) 분석 부분과 여러가지 예외 / 에러 처리 등이다.




많은 이들에게 도움이 되었으면 한다.


Posted by OOJJRS
,