Skip to main content

C++ of the Day #25 - Boost::Iterator 라이브러리 사용하기 #1

이번 시리즈에선 boost::iterator 라이브러리를 소개합니다.

만약 stl에서 제공하지 않는 컨테이너를 만들 필요가 있다면 이 컨테이너에서 stl과 호환되는 iterator를 제공하는 것이 컨테이너의 사용자들에겐 큰 도움이 됩니다. 가장 간단한 이유는 stl의 알고리즘들을 그대로 사용할 수 있기 때문이죠.

하지만 이런 iterator 클래스를 만드는 것이 만만치 않은 작업인데 이를 도와주는 것이 바로 boost::iterator 라이브러리입니다. ((boost::iterator에는 이외에도 새로운 concept의 iterator_traits등 다른 목적들도 있습니다.))

이번 글을 위해 만들어 볼 컨테이너가 하나 필요한데 여기서는 실제 컨테이너에 대해 sorting된 view를 제공하는 sorted_view 컨테이너를 설계해 보겠습니다.

실제로 copy에 들어가는 비용이 높은 element를 가진 컨테이너를 sorting하는 것은 성능에 문제가 될 수 있습니다. 이 컨테이너의 iterator만을 가지고 따로 컨테이너를 만들어 sorting을 하는 방법을 사용하면 실제 copy는 iterator에 대해서만 일어나므로 이 문제를 해결할 수 있습니다. 문제는 이 iterator를 가지고 만든 컨테이너의 사용법이 원래 컨테이너와 달라진다는 것입니다. 그럼 시작해 보실까요?

완성된 sorted_view의 사용법은 다음과 같습니다.
using namespace std;
using namespace boost::lambda;

list<int> li;
li.push_back(3);
li.push_back(0);
li.push_back(1);
li.push_back(4);
li.push_back(2);

for_each(li.begin(), li.end(), cout << _1 << " ");
cout << endl;

typedef sorted_view<list<int>::iterator> sv_t; // (1)

sv_t sv1(li.begin(), li.end());
for_each(sv1.begin(), sv1.end(), cout << _1 << " "); // (2)
cout << endl;

sv_t sv2(li.begin(), li.end(), greater<int>()); // (3)
for_each(sv2.begin(), sv2.end(), cout << _1 << " ");
cout << endl;

sv_t::const_iterator i = sv2.begin();
i += 2; // (4)
cout << *i << endl;
cout << i - sv2.begin() << endl;

// output
// 3 0 1 4 2 
// 0 1 2 3 4 
// 4 3 2 1 0 
// 2
// 2
일반 컨테이너와 같은 방법으로 사용할 수 있음을 알 수 있습니다.(2) 게다가 list는 bidirectional traversal iterator만을 제공하는데 비해 sorted_view의 iterator는 random access traversal iterator를 제공하고 있습니다.(4) ((현재 표준에서는 bidirectional iterator 혹은 random access iterator라고 부르고 있으나 boost::iterator 라이브러리는 이 category를 traversal에 관한 것과 value access의 orthogonal한 두 타입에 대해 분류하고 있습니다.)) 또한 두번째 sorted_view의 생성자에서 알 수 있듯이 iterator pair외에 sorting시 비교에 사용할 function object를 optional 인자로 가지고 있습니다.(3) ((이 라인은 boost::lambda를 사용하여 다음과 같이 쓸 수 있습니다.
sv_t sv2(li.begin(), li.end(), _1 > _2);
)) 마지막으로 sorted_view의 template parameter는 실제 컨테이너의 iterator 타입입니다.(1) 여기서 template parameter를 바로 컨테이너 타입을 받을 수도 있었으나 다음과 같이 컨테이너가 아닌 일반 배열을 가지고도 사용할 수 있도록 iterator 타입을 받도록 했습니다.
int data[] = { 3, 0, 1, 4, 2 };
int const N = sizeof(data) / sizeof(data[0]);

sorted_view<int*> sv3(data, data + N);
for_each(sv3.begin(), sv3.end(), cout << _1 << " ");
cout << endl;

// output
// 0 1 2 3 4 
그럼 이제 sorted_view를 구현해 볼까요?
template <class Iter>
class sorted_view
{
  typedef Iter C_iter;
  typedef typename boost::iterator_value<c_iter>::type C_value_type;
  typedef boost::function<bool (C_value_type const&, C_value_type const&)> Compare;
public:
  sorted_view(C_iter first, C_iter last, Compare comp = std::less<c_value_type>())
      : comp_(comp) {
    using namespace boost::lambda;

    for (C_iter i = first; i != last; ++i) {
    data_.push_back(i);
  }

  std::sort(data_.begin(), data_.end(), bind(comp_, *_1, *_2));
}
private:
  std::vector<c_iter> data_;
  Compare comp_;
};
먼저 클래스는 세개의 typedef로 시작합니다. 이중 Compare는 비교를 위해 사용될 function object를 저장하기 위한 boost::function 타입입니다. 그리고 sorted_view의 생성자를 보면 이 Compare 인자의 default value는 std::less<> 임을 알 수 있습니다. 그리고 생성자의 body에서는 간단하게 먼저 입력된 iterator pair를 사용해서 vector를 만들고 이 vector를 사용해 sort()를 호출하고 있습니다. 여기서 data_의 value_type 자체가 C_iter이기 때문에 *_1과 *_2를 사용하여 C_iter 자체가 아닌 C_iter의 value_type, 즉 *C_iter 타입으로 sorting하고 있음을 알 수 있습니다. ((여기서 사용된 bind, _1, _2 같은 문법은 boost::lambda를 사용한 것입니다.)) 이제 sorting된 iterator의 vector를 가진 sorted_view가 완성되었습니다. 다음으로 할 일은 이 컨테이너에 begin()과 end()를 제공하여 다음과 같은 코드가 동작하도록 하는 것입니다.
for_each(xx.begin(), xx.end(), cout << _1 << " ");

for (i = xx.begin(); i != end(); ++i) {
cout << *i << " ";
}
그럼 다음과 같이 간단히 sorted_view가 가지고 있는 vector의 iterator를 사용자에게 주는 것은 어떨까요?
template <class Iter>
class sorted_view
{
  ...
  typedef typename std::vector::const_iterator const_iterator;

  const_iterator begin() const {
    return data_.begin();
  }

  const_iterator end() const {
    return data_.end();
  }
  ...
이 경우 위에서 살펴봤던 코드는 컴파일 에러가 발생합니다. 이유는 이 iterator의 value_type은 원래 컨테이너의 iterator이기 때문이죠. 따라서 위의 코드를 굳이 사용하겠다면 다음과 같이 수정해 주어야 합니다.
for_each(xx.begin(), xx.end(), cout << *_1 << " ");

for (i = xx.begin(); i != end(); ++i) {
  cout << **i << " ";
}
일반 컨테이너와는 iterator를 dereference하는 방법이 다르네요. 일반 컨테이너는 *를 하나만 쓰면 되지만 이 경우에는 두개의 *를 써주어야 합니다. 게다가 element의 멤버 함수를 -> 를 사용해서 부르려면 일반 컨테이너의 i->p() 와는 다르게 (*i)->p() 라고 써야 합니다.

이제 왜 iterator 클래스가 필요한지 확인했습니다. 이미 이번 글은 너무 길어졌네요. 다음 글에서 sorted_view의 iterator를 구현해 보도록 하겠습니다. ;-)

Comments

Popular posts from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1...

CodeHighlighter plugin test page.

This post is for testing CodeHighlighter plugin which uses GeSHi as a fontifier engine. ((Those code blocks are acquired from Google Code Search .)) ((For more supported languages, go CodeHighlighter plugin or GeSHi homepage.)) C++ (<pre lang="cpp" lineno="1">) class nsScannerBufferList { public: /** * Buffer objects are directly followed by a data segment. The start * of the data segment is determined by increment the |this| pointer * by 1 unit. */ class Buffer : public PRCList { public: Buffer() { ++index_; } PHP (<pre lang="php" lineno="4">) for ($i = 0; $i $value = ord( $utf8_string[ $i ] ); if ( $value < 128 ) { // ASCII $unicode .= chr($value); } else { if ( count( $values ) == 0 ) { $num_octets = ( $value } $values[] = $value; Lisp (<pre lang="lisp">) ;;; Assignment (define-caller-pattern setq ((:star var fo...

C++ of the Day #43 - SQLite3 C++ wrapper #1

The Definitive Guide to SQLite 를 읽다가 공부 겸 해서 C++ wrapper를 만들어 보았습니다. 최대한 C++ 냄새(?)가 나도록 만들어 보았습니다. :-) ((SQLite는 복잡한 관리가 필요없이 사용가능한, 파일이나 메모리 기반의, 라이브러리로 제공되는, 약 250kb 용량의, 대부분의 SQL92문을 지원하는, open source RDB입니다.)) 이 wrapper를 사용하기 위해서는 (당연하게도!) sqlite3 와 (당연하게도?) boost 라이브러리가 필요합니다. 사용 예들을 살펴보는 것으로 설명을 대신합니다. 이번 글에서는 다음과 같은 contacts 테이블이 test.db에 존재한다고 가정합니다. CREATE TABLE contacts ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, phone TEXT NOT NULL, UNIQUE(name, phone) ); Command 먼저 test.db 파일을 사용하기 위해 다음과 같이 파일 이름을 주어 connection 객체를 생성합니다. 생성과 동시에 test.db와 연결이 이루어집니다. ((생성자외에 open() 함수를 사용할 수도 있습니다.)) sqlite3pp::connection conn("test.db"); 다음은 contacts 테이블에 정보를 추가하는 가장 간단한 방법입니다. connection 클래스에서 제공하는 execute 함수를 사용합니다. ((executef 함수를 사용하면 printf와 같은 문법을 사용하여 query문을 작성할 수 있습니다.)) conn.execute("INSERT INTO contacts (name, phone) VALUES ('user', '1234')"); 위와 동일한 작업을 parameterized query를 사용하여 할 수도 있습니다. ((step()함수가 실제 query문을 수행하는 함수입니다. ...