STL.NET Primer (2/4) 에서 이어지는 글입니다.
기반 지식 정의
STL.NET 을 알아가는 데는 두 가지 방법이 있습니다: 하나는 STL과 STL.NET의 차이점을 알아보는 것이고, 다른 하나는 STL과 STL.NET이 공통점을 알아보는 것입니다. 이 둘의 차이점을 나열하는 것은 이미 STL을 맛보았던 사람에게만 와닫을 법하기 때문에, (독한 연기로 질식시키는 듯한) 라이브러리의 낯선 부분에 대한 설명은 피하는 것이 좋을 듯 합니다. 말하자면, 컨테이너의 세밀한 특징, 그리고 System 컬렉션 라이브러리와의 상호운용 원리라는 난해한 부분에 대해서는 설명을 자제하겠다는 뜻입니다. 물론 이들 사항도 흥미로운 부분이긴 합니다. 하지만 이들 사항에 대한 설명은 이 라이브러리에 깊이 빠진 새로운 누군가의 몫으로 남겨두는 편이 더 났지 않을까요? 이것이 바로 아래에 이어질 내용 - 입문자를 위한 내용- 에 담긴 저의 의도입니다. 이런 방법을 취함으로써 이 라이브러리에 처음 발을 내딛는 사람은, STL과 STL.NET 모두가 제공하는 확장된 모델, 즉 매개변수화된(parameterized) 컬렉션에 기분좋게 다가갈 수 있을 것입니다.
그렇다면 STL과 STL.NET이 공유하는 부분은 무엇일까요? 이 둘 모두 순차(sequential) 컨테이너와 연관(associative) 컨테이너이란 두 개의 기본 컴포넌트와 지네릭 알고리즘으로 구성되어 있다는 것입니다. (맞습니다. 여러분이 STL에 익숙한 개발자라면, 어떤 내용이 이어질지 알고 있을 것입니다. 그렇다 하더라도, 이 절은 기본 용어와 기반 지식을 설명하는 데 할애되었기 때문에, 그러한 여러분의 인내심을 요청하는 바입니다.) 지네릭 알고리즘은 컨테이너 타입에 직접적으로 운용되지 않습니다. 그 대신, 이들 알고리즘에 운용할 요소의 범위를 나타내는 반복자(iterator)가 넘어가는데, 통상 이들 반복자를 가리켜 first와 last라 칭합니다. 공식적으로 좌측 포함 간격(left-inclusive interval)이라 이름붙은 아래의 요소 범위 표기법은,
// "first와 last까지의 모든 요소를 포함하지만 last는
// 포함하지 않습니다." 라는 의미를 갖습니다.
[ first, last )
C++
복사
범위가 first에서 last까지지만 (first와는 달리) last는 포함되지 않음을 나타냅니다. first와 last가 같은 경우에는 그 범위 안에 어떠한 요소도 없다는 뜻입니다.
순차(sequential) 컨테이너에는 단일 타입으로 이루어진 정돈된 요소들이 담겨 있습니다. 가장 기본적인 순차 컨테이너는 vector와 list 타입입니다. (세 번째 순차 컨테이너인 - '데크'라고 읽습니다 - 는 vector처럼 동작하지만, 맨 앞 요소에 대한 효율적인 삽입과 삭제에 특화되어 있습니다. 예를 들어, 큐(queue)를 구현할 때는 vector보다는 deque가 더 났습니다.)
순차 컨테이너 타입를 참조하기에 앞서 해야할 것은, 다음의 헤더 파일 중에서 적절한 파일을 포함하는 것입니다.
#include <cli/vector>
#include <cli/list>
#include <cli/deque>
C++
복사
이들 헤더 파일에는 interface_vector와 같은 공유되는 기초 인터페이스의 선언부와, generic_vector등의 이들 컨테이너의 형제뻘 컨테이너도 함께 담겨 있습니다.
STL.NET 컨테이너는 참조 타입입니다. 컨테이너 선언부에는 트래킹 핸들(tracking handle)이 있는데, 이는 자동적으로 nullptr로 초기화됩니다. 그리고 우리는 gcnew 연산자를 이용하여 실제 컨테이너를 할당하게 됩니다. 이전 절에서 이미 이를 간단하게 보여주긴 했지만, 여기에 한번 더 명시하도록 하죠.
void f()
{
// 빈 vector를 할당합니다 . . .
vector<String^>^ = gcnew vector<String^>;
// 기본적으로 nullptr로 각각 설정되어 있는
// 10개 요소가 담긴 리스트를 할당합니다.
list<Object^>^ olist = gcnew list<Object^>( 10 );
// 트래킹 핸들은 nullptr로 자동 설정됩니다.
deque<int>^ ideck;
// 뭔가 흥미로운 일을 합니다 . . .
};
C++
복사
연관 컨테이너에 대한 선언법과 용법에 관해서는 이 연재물의 다음 컬럼에서 다루도록 하겠습니다.
연관 컨테이너는 요소의 보관과 되찾기(retrieval)에 대한 질의(query)를 효율적으로 지원합니다. 기본적인 두 가지 연관 컨테이너 타입은 map과 set이죠. map은 key와 value 쌍으로 이루어져 있습니다. key는 검색을 위해 사용되고, value에는 저장하고 되찾아올 데이터가 담깁니다. 예를 들어, 전화번호부는 손쉽게 map을 이용하여 나타낼 수 있는데, 여기서 key는 개개의 이름을 나타내고, value는 그 이름에 연관된 전화 번호를 나타냅니다.
map은 자신의 기초를 이루는 트리 추상체(tree abstraction)을 이용하여 요소를 오름순으로 정렬합니다. hash_map 컨테이너는 되찾기(retrieval) 명령에 있어 좀더 효율적입니다. 하지만 hash_map의 반복(iteration)은 다소 임의적인 순서(random order)로 각 요소에 접근(access)합니다. 만약 되찾기가 주된 목적이라면 hash_map을 사용하는 편이 좋습니다.
set은 단일 key 값들로 구성되며, 그 값이 존재하는지에 대한 질의(query)에 효과적입니다. 예를 들어, 텍스트 질의 시스템(text query system)을 만들 경우에는 텍스트에 담긴 단어들로 이루어진 데이터베이스를 구축하기 위해선 the, and, but등과 같은 제외시킬 일반 어휘 목록이 필요할 것입니다. 이 프로그램은 텍스트에 담긴 각 단어를 차례로 읽어, 읽어낸 단어가 제외 단어 목록에 있는지를 검사하고, 질의 결과에 따라 그 단어를 데이터베이스에 저장하거나 버릴 것입니다. set뿐만 아니라, hash_set이란 컨테이너도 있는데, 이들 간의 일반적 특징은 map과 hash_map간에 보이는 일반적 특징과 동일합니다.
map과 set에 담길 각 key는 각기 서로 달라야 합니다. 하지만 multimap과 multiset은 중복된 key를 허용합니다. 예를 들어, 위의 전화번호부는 한 개인에 속한 여러 사항이 기재될 수 있어야 할 것입니다. 바로 이 경우에 multimap을 사용하면 됩니다. 이밖에도 hash_multimap과 hash_multiset이란 컨테이너가 있습니다.
이들 컨테이너 타입에 대한 헤더 파일은 다음과 같습니다.
//map과 multimap의 경우
#include <cli/map>
//set과 multiset의 경우
#include <cli/set>
//hash_map과 hash_multimap의 경우
#include <cli/hash_map>
//hash_set과 hash_multiset의 경우
#include <cli/hash_set>
C++
복사
간단한 데모
이제 예제를 다루면서 좀더 구체적으로 들어가보도록 하죠. 다음은 텍스트 파일에 있는 단어의 개수를 세는 간단한 프로그램입니다. 이 프로그램은 map과 hash_set, 그리고 vector을 어떻게 사용하는지를 보여주고 있습니다. 함수 하나가 다음과 같이 선언되어 있습니다.
map<String^, int>^
build_word_count( vector<array<String^>^>^ words,
array<String^> ^common_words )
C++
복사
템플릿 구문(syntax)은 여러분이 이에 익숙해지기 전까지는 정신 없이 보일 것입니다. 이제 이를 분석해보도록 하죠. build_word_count()의 반환 타입은 map으로서, 이 map의 key는 이 파일의 각기 고유한 단어를 나타내는 문자열(string)이고, value는 해당 단어가 담긴 횟수를 나타내는 정수입니다. 이 함수의 첫 번째 매개변수는 이 파일에 담긴 각 단어들을 문자열 요소로 갖는 CLI 배열 벡터입니다. 두 번째 매개변수는 단어 개수 세기에 있어 제외시킬 단어들의 모음입니다.
위 선언문에는 여러 ^(hat이라고 부르더군요. 역주)이 닫기 괄호와 함께 널려 있기 때문에 복잡해 보입니다. 이 문장은 한쌍의 typedef을 이용하여 단순화할 수 있습니다.
typedef array<String^>^ words;
typedef vector<words>^ text;
C++
복사
위 typedef문을 이용하여 선언하면 다음과 같을 것입니다.
map<String^ int>^
build_word_count( text theText, words common )
C++
복사
명시적으로 선언된 map 또한 제거할 수 있지만, 이 컬럼을 읽는 여러분에게 연습이 되라고 놔두었습니다. 다음은 위 함수를 구현할 차례입니다. 우리는 이 작업을 두 부분으로 나눌 것입니다: 아래에 보이는 바와 같이, (1)우리의 map과 hash_set에 대한 초기화, 그리고 (2)텍스트 그 자체에 대한 실제 처리입니다.
// 항목 #1: 컨테이너에 대한 초기화
// 크기가 얼마나 될지 모르는 상태에서 비어있는 map을 할당합니다 . . .
map<String^, int> ^wordOccur = gcnew map<String^, int>
// 배열에 담긴 요소들로 hash_set을 채웁니다.
hash_set<String^> ^comWords =
gcnew hash_set<String^>(
&common[0],
&common[common->Length]);
C++
복사
여러분이 STL에 익숙하지 않다면, hash_set에 대한 초기화 부분이 지저분하게 보일 것입니다. 하지만 구문을 떼어놓고 본다면, 사실 이 선언문은 직관적이고도 매우 강력합니다.
우리가 원하는 것은 hash_set을 배열에 담긴 요소들로 초기화하는 것입니다. 물론 우리는 이를 for each 문을 이용하여 각 요소들을 방문(iterating)함으로써 명시적으로 이뤄낼 수 있습니다. 예를 들어,
// hash_set문을 단순화합니다.
// 파트 #1: 빈 hash_set을 할당합니다.
hash_set<String^> ^comWords = gcnew hash_set<String^>;
// 파트 #2: 배열의 요소들로 hash_set을 채웁니다.
for each ( String^ wd in common )
comWords->insert( wd );
C++
복사
배열에 담긴 첫 번째 요소의 주소, 그리고 마지막 유효 요소를 하나 넘긴 요소의 주소를 hash_set 생성자에 넘김으로써, hash_set을 동일하게 채우게 됩니다. 이 두 개의 주소는 hash_set 생성자가 방문(iterating)할 요소들의 범위를 알려주어, 이들 요소를 이 컨테이너에 넣습니다. 이것이 바로 이 절 처음에서 제가 언급했던 반복자 범위(iterator range)에 관한 관용구입니다.
위 코드의 두 번째 단은 텍스트를 실질적으로 처리하는 부분입니다. 아직 우리는 반복자를 공식적으로 다루지 않았기 때문에, 이제부터 흔히 많이 쓰는 for 루프를 이용하여 vector 안을 순회해 보겠습니다. 우리는 for each 문을 사용하여 vector에 담긴 각각의 배열 요소에 접근할 것입니다. 다음은 이에 관한 코드입니다.
// 각각의 배열에 차례로 접근합니다.
for ( int ix = 0; ix < theText->size(); ++ix )
{
for each ( String^ wd in theText[ ix ] )
if ( common->find( wd ) == common->end() )
word_map[ wd ]++;
}
// word_map을 반환해야 한다는 것을 잊지 맙시다 . . .
return word_map;
C++
복사
find()는 hash_set 멤버 함수로서, 특정 아이템이 이 컨테이너에 존재하는지를 알아내는 데 사용합니다. 참고적으로, 모든 연관 컨테이너는 find() 멤버를 지원합니다. (맞습니다. 기본(built-in) 배열을 포함한 순차 컨테이너, 그리고 STL/STL.NET 모델에 통합 가능한 모든 컬렉션에는 find() 지네릭 알고리즘을 사용할 수 있습니다. 이러한 알고리즘과 컨테이너의 분리는 실제보다는 이론적으로 더욱 명확합니다. 이는 특히 list 순차 컨테이너에서 그러합니다.) 이 멤버 함수는 제가 아직 언급하지 않았던 것, 즉 이 컨테이너의 반복자를 반환합니다. 우선은 반복자를 포인터 타입이라고 생각해보기로 하죠. 찾고자 하는 아이템이 컨테이너 안에 있다면, find()는 그 아이템에 대한 반복자를 반환하고, 그렇지 않다면 컨테이너의 마지막에서 하나 지난 아이템에 대한 반복자를 반환합니다. 이 값은 end()가 반환하는 반복자 값과 동일합니다. STL에서의 요소 검색의 성공 여부에 대한 결정은 검색 메소드가 반환하는 반복자와 end()가 반환하는 반복자를 비교하는 것으로 이루어집니다. 만약 이들 두 반복자의 값이 동일하다면, 그 요소는 컨테이너에 존재하지 않는 것입니다.
모든 컨테이너에는 begin()와 end() 멤버 함수가 있습니다. begin()은 컨테이너의 첫 번째 요소에 대한 반복자를 반환하고, 이미 제가 말했다시피, end()는 컨테이너의 마지막 요소를 하나 넘긴 요소의 반복자를 반환합니다. 다음은 어떻게 반복자를 선언하는지와, 어떻게 이들 두 멤버를 이용하여 초기화하는지를 보여줍니다.
vector<words>::iterator first = theText->begin();
vector<words>::iterator last = theText->end();
C++
복사
컨테이너 내부 순회는, 보통 for나 while 루프에서 first를 증가시켜, first와 last가 같아졌을 때 끝내는 것으로 이루어집니다. 예를 들어,
for ( ; first != last; ++first )
C++
복사
반복자 쌍이 반드시 필요한 이유는 증가 연산자에 대한 반복적인 적용을 통해 first에서 시작하여 last까지 도달 할 수 있어야 하기 때문입니다. 컴파일러 자신이 이를 강제할 수는 없기 때문에, 이러한 요구 사항을 만족시키지 못한다면 실행시(run-time)에 정의되지 않은(undefined) 행동을 보이게 되는 결과를 초래합니다.(밑줄친 이 부분은 해석이 애매하게 된 부분입니다.. 실력이 딸려서리.. 역주)
요소 자체에 대한 접근(access)은 역참조 연산자(*)를 통해 반복자를 참조함으로써 이룹니다. 예를 들어, 우리의 벡터에서 각각의 CLI 배열 요소를 되찾고자 한다면, for 루프 안에 다음과 같이 기입하면 됩니다.
array<String^> ^as = *first;
C++
복사
연관 컨테이너에 대한 자세한 선언법과 용법은 이 연재물의 나중 컬럼에서 다루도록 하겠습니다.
STL.NET Primer (4/4)으로 이어집니다.