대/소문자 문자열 변환

2011. 6. 6. 09:38C++


원본 : http://cpplog.tistory.com/14

strupr, strlwr함수를 사용한 대/소문자 문자열 변환 방법은 대부분 잘 알고 있을 것이다. 아래 예는 문자열 "small"을 대문자로 변환하는 코드이다.

char str[] = "small";
strupr(str);

STL의 string 클래스와 transform 알고리즘을 사용하여 좀 더 C++답게 만들어 보자.

std::string str("small");
std::transform(str.begin(), str.end(), str.begin(), toupper);

그러나 이 코드는 영어가 아닌 다른 언어(독일어, 스페인어, 이탈이아어 등등)에서는 문제가 된다. 예를들어 small의 스페인어에 해당하는 단어는 pequeño(구글 번역기로 돌려 본 결과임)이다.  이것을 위의 코드로 돌려보면 원하는 결과는 PEQUEÑO인데 실제 결과는 PEQUEñO이다.  ñ이 Ñ으로 제대로 변환되지 않았다.

// 다국어 처리를 위해 문자형(character type)을 wchar_t로 정함 
std::wstring str(L"pequeño");
std::transform( str.begin(), str.end(), str.begin(), towupper);

이런 결과가 나오는 이유는 로캘(locale) 설정 때문이다. 로캘은 지역 또는 국가의 화폐, 시간, 숫자, 언어 등의  정보이다.  로캘은 setlocale 함수로 설정할 수 있고 기본 값은 setlocale(LC_ALL, "C")을 호출한 것 같다. ("C" 로캘은 모든 문자 데이터 형이 1 바이트이고 그 값은 항상 256 보다 작다.)

정확한 결과를 얻기 위해 로캘을 스페인어(spanish)로 지정해보자.

char* org_lc = setlocale(LC_CTYPE, NULL);
setlocale(LC_CTYPE, "spanish");
std::wstring str(L"pequeño");
std::transform( str.begin(), str.end(), str.begin(), towupper); 
setlocale(LC_CTYPE, org_lc);

setlocale(LC_CTYPE, "spanish")을 호출하기 전에, setlocale(LC_CTYPE, NULL)을 호출하여 이전 로캘을 보관한다. 그 이유는 setlocale은 전역 로캘을 변경시키는 함수이기 때문에 로캘을 변경하기 전에 이전 로캘을 보관했다가 원하는 처리후 다시 원래의 로캘으로 복구 시켜 주는게 좋다.  이 방법의 단점은 전역 로캘을 변경시키기 때문에 멀티스레드 환경에서 문제가 발생할 수 있다는 것과 이전 로캘을 유지하기 위한 코드가 들어가야 한다는 것이다. 

이러한 문제를 해결하기 위해 전역 로캘 대신 지역 로캘을 사용할 필요가 있다. STL에는 지역 로캘로 사용할 수 있는 locale 클래스가 존재한다. 그리고 이 locale 클래스를 매개 변수로 받는 toupper 템플릿 함수가 존재한다.

template<class CharType> CharType toupper(CharType ch, const locale& loc);

locale 클래스와 toupper 템플릿 함수를 적용해보자.

std::locale loc("spanish");
std::wstring str(L"pequeño");
std::transform(str.begin(), str.end(), str.begin(), std::bind2nd(std::ptr_fun(std::toupper<wchar_t>), loc));

transform함수는 네번째 인자는  매개 변수가 하나인 unary function을 인자로 받는데 toupper 템플릿 함수의 인자가 두개이므로 특별한 처리가 필요하다. toupper<wchar_t> 함수 포인터(function pointer)를 함수 객체(function object)로 바꾸는 ptr_fun과 locale의 인스턴스 loc를 두번 째 인자로 묶어주는 bind2nd함수를 이용한다.

점점 C++ 다워지는 것 같지 않은가? 그렇지만 다소 복잡한 느낌이 든다. 이걸 보면서 머리에 쥐나는 사람도 있을 것이다. 걱정하지 말자. 위의 예 보다 좀 더 표준적이고 사용하기 쉬운 방법이 있다. 바로 패싯(facet)을 이용하는 방법이다. 패싯이라는 용어가 좀 난해한데 사전에 찾아보면 면(측면)이라는 뜻이다. 여기서는 쉽게 설명하면 "로캘의 OO 측면" 이라고 말할 때 OO에 해당하는 화폐, 시간, 숫자, 언어 등의 로캘의 세부 사항이 패싯이 된다. 

use_facet 함수를 사용하면 locale 인스턴스에서 원하는 facet 객체를 얻을 수 있다. 소문자를 대문자로 변경하기 위해 문자 타입을 위한 facet, 즉 ctype 객체를 얻어서 그 멤버인 toupper를 호출한다.

std::locale loc("spanish");
const std::ctype<wchar_t>& ct = std::use_facet<std::ctype<wchar_t> >(loc);
std::wstring str(L"pequeño");
ct.toupper(str.begin(), str.end());

대/소문자 문자열 변환이 쉬운듯 하면서 제대로 하려면 어렵다는 것을 느끼게 된다. locale과 facet에 대해 잘 몰랐다면 지금이라도 늦지 않았으니 알아두는 것이 좋을 것이다. 특히 다국어를 지원할 소프트웨어를 개발 중이거나 개발하려고 한다면 반드시 알아두는게 좋다.

[추가]
이 글을 포스팅하고 긴급히 추가를 하게 되었다. 왜냐하면 마지막 방법은 표준을 만족 못하는 잘못된 예이기 때문이다. ctype의 toupper 멤버 함수의 프로토타입을 보면 다음과 같다.

const CharType *toupper(CharType* first, const CharType* last) const;

매개 변수로 CharType* 형을 사용하는데 basic_string<CharType>의 iterator를 인자로 사용 가능하더라도, 표준에서는 이것이 올바르게 동작한다고 보장하지 않는다. facet을 사용한 정확한 예로 다시 고치면 다시 transform 알고리즘을 사용해야 한다. 여전히 복잡하다.

std::locale loc("spanish");
const std::ctype<wchar_t>& ct = std::use_facet<std::ctype<wchar_t> >(loc);
std::wstring str(L"pequeño");
std::transform(str.begin(), str.end(), str.begin(), 
    std::bind1st(std::mem_fun(&std::ctype<wchar_t>::toupper), &ct));

이것을 범용적으로 사용할 수 있는 함수로 만들면 도움이 된다. 직접 만들어도 되겠지만, boost의 string algorithm을 사용할 것을 추천한다.  

std::locale loc("spanish");
std::wstring str(L"pequeño");
boost::to_upper(str, loc);