Skip to main content

C++ of the Day #34 - dynamic visitor 2

이번 글에서는 지난번 글에서 살펴보았던 dynamic visitor를 개선해 보도록 하겠습니다. 먼저 이전 구현에서 불편한 점을 알아보겠습니다.


struct B {}
struct D1 : B {}
struct D2 : B {}

void print(D1* ) { cout << "D1n"; }
void print(D2* ) { cout << "D2n"; }


이전과 같은 B, D1, D2 클래스가 있습니다. 달라진 점은 print라는 함수가 D1과 D2 클래스에 대해 이미 존재한다는 점입니다.

이 클래스들을 가지고 dynamic_visitor를 사용하면 다음과 같습니다.


typedef mpl::vector TL;

struct PrintVisitor : dynamic_visitor
{
void visit(B* b) { cout << “Bn”; }
void visit(D1* b) { cout << “D1n”; }
void visit(D2* b) { cout << “D2n”; }
};


여기서 문제는 이미 D1, D2에 대해 print라는 함수가 구현되어 있는데 PrintVisitor에서 다시 구현해야 한다는 점입니다. 물론 이 예제에서는 코드가 짧아 별 문제 없어 보이지만 긴 코드라면 문제가 됩니다. DRY 규칙에 어긋나는 것이죠. ((Don't Repeat Yourself.))

그럼 이번 글에서 살펴볼 개선된 dynamic_visitor의 사용법을 보겠습니다.


typedef mpl::vector TL;

struct PrintVisitor : dynamic_visitor
{
PrintVisitor() {
assign_impl(print);
assign_impl(print);
}
void visit(B* b) { cout << “Bn”; }
};


만약 base 클래스인 B 클래스에 대한 visit함수가 아무 일도 안해도 된다면 다음과 같이 using을 사용하여 B*에 대한 visit 함수 대신 dynamic_visitor가 제공하는 default 구현을 사용할 수 있습니다.


typedef mpl::vector TL;

struct PrintVisitor : dynamic_visitor
{
using super_t::visit;

PrintVisitor() {
assign_impl(print);
assign_impl(print);
}
};


여기서 super_t 타입은 dynamic_visitor에서 제공하는 typedef입니다. 만약 이 super_t 타입이 없다면 다음과 같이 코드가 복잡하고 길어지겠죠?


using dynamic_visitor::visit;


그럼 이제 구현에 대해 살펴보겠습니다.


template
class dynamic_visitor
{
typedef typename mpl::back::type base_type;

private:
template
struct visit_impl // (1)
{
typedef Ret (*impl_type)(T*);
visit_impl(impl_type it = 0) : impl(it) {}
impl_type impl;
};

typedef typename mpl::inherit_linearly<
TL,
mpl::inherit<_1, visit_impl<_2> >
>::type impl_map; // (2)

template
typename visit_impl::impl_type& get_impl(visit_impl& t) // (3)
{
return t.impl;
}

impl_map map_;

protected:
typedef dynamic_visitor super_t; // (4)

virtual Ret visit(base_type* ) {} // (5)

template
void assign_impl(typename visit_impl::impl_type impl) // (6)
{
get_impl(map_) = impl;
}

public:
Ret operator()(base_type* b) {
return do_visit(b, mpl::false_());
}

private:
template
Ret do_visit(base_type* b, mpl::false_) {
typedef typename mpl::front::type Head;
typedef typename mpl::pop_front::type Tail;
typedef typename mpl::empty::type IsEmpty;

Head* p = dynamic_cast(b);
if (p != 0) {
if (get_impl(map_)) { // (7)
return get_impl(map_)(p);
}
return static_cast(this)->visit(p);
}

return do_visit(b, IsEmpty());
}

template
Ret do_visit(base_type* b, mpl::true_) {
std::cout << "assertn"; // handle error.
return Ret();
}
};


각 항목 번호는 위의 코드에 있는 주석에 있는 번호에 해당합니다.


  1. function pointer를 저장하고 있는 클래스로 T 타입에 대해 Ret (T*) 함수 포인터를 저장한다.

  2. TypeList에 있는 타입들을 멤버 변수로 가지는 클래스를 선언한다. 여기서 inherit_linearly는 mpl 라이브러리에서 제공하는 meta-function으로 각 타입을 가지는 클래스들을 차례대로 상속받도록 fold하여 result 타입엔 각 타입에 해당하는 값을 가지고 있게 만들어 준다. ((예를 들어 만약 TL이 mpl::vector이라면 다음과 같은 클래스 hierarchy가 만들어집니다.

    struct visit_impl : empty_base { Ret (*impl)(char*); };
    struct visit_impl : visit_impl { Ret (*impl)(int*); };
    struct visit_impl : visit_impl { Ret (*impl)(double*); };

    ))

  3. 2번에서 만든 타입에서 특정 타입을 키로 하여 값을 얻을 수 있도록 해주는 helper function이다.

  4. 위에서 살펴 봤던 super_t 타입을 typedef한다.

  5. base 클래스에 대한 기본 visit 구현을 제공한다. 하위 클래스에서 using super_t::visit;를 하면 이 함수가 하위 클래스에서 보이게 된다.

  6. 하위 클래스에서 구현 함수를 셋할 수 있도록 해주는 protected 함수이다.

  7. 실제 operator() 호출시 사용자에 의해 셋된 구현이 있는지 확인하고 있다면 그 함수를 호출한다. 없다면 이전 버전과 동일하게 사용자가 구현한 visit 함수를 호출한다.



이외에 이전 글의 dynamic_visitor에 비해 개선된 점은 원하는 리턴 타입을 사용자가 지정할 수 있다는 점입니다. 예를 들어 추가하고자 하는 함수가 char const*를 리턴해야 한다면 다음과 같이 할 수 있습니다.


char const* get_name(D1* ) { return "D1"; }

typedef mpl::vector TL;

struct PrintVisitor : dynamic_visitor
{
char const* visit(B* ) { return "B"; }
char const* visit(D2* ) { return "D2"; }
};

// usage
PrintVisitor pv;
cout << pv(b1) << endl;
cout << pv(b2) << endl;


이번 글에서는 개선된 dynamic_visitor에 대해 알아봤습니다.

개인적으로는 남들이 사용할 수는 있지만 수정할 수 없는 라이브러리 코드에서 사용하는 것 외의 상황에서 Visitor 패턴을 만들어 사용하는 것을 권장하지 않습니다. 한번 프로젝트에서 사용했다가 크게 후회했던 적이 있거든요. 사실 제가 수정할 수도 있는 코드였는데 괜히 이 디자인 패턴을 사용해 봤던 것이죠. 덕분에 기능 추가할때마다 Visitor를 구현해야 했고 디버깅시 trace도 꽤나 복잡해지더군요. 물론 코드 읽기도 어려워지고요.
그리고 라이브러리 코드라고 해서 항상 코드를 수정할 수 없는 것은 아니라는 점도 고려해서 기능 확장을 위해 Visitor가 필요한지 그냥 코드를 수정하게 할지를 결정해야 할 것 같습니다.

Downloads



dynamic_visitor.zip

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...

Coverity Prevent - 소스 코드 검사 도구

이번에 회사에서 소스 코드 검사 도구를 하나 구입하는데 이것저것 비교해본 결과 Coverity Prevent 라는 툴이 가장 좋아 이걸로 결정했다고 하더군요. 간단히 말하자면 Lint 처럼 소스 코드에 대해 static analysis를 하는 도구입니다. 그래서 시험삼아 돌려본 결과를 보게 되었는데 그 품질이 정말 놀랍군요. 메모리 릭이나 초기화하지 않은 변수, 버퍼 오버런, null 체크등등 거의 정확하게 에러를 보고해 줍니다. 다음은 전체 코드에서 발견된 문제점들을 리스트 형태로 보여주는 화면입니다. 정말 많군요. ;-) 다음은 이중 하나의 에러를 자세히 보기 위해 선택하여 들어간 화면입니다. 소스 코드에 라인번호와 에러가 발생한 위치, 에러가 발생할 수 있는 조건들을 자세히 보여줍니다. 정말 '와우!'입니다. 각 함수와 같은 이름들을 클릭하면 그 함수로 바로 이동할 수 있습니다. 사용해보니 개인적으로는 purify 같은 runtime 검사 도구보다 더 맘에 드는군요. 사실 Lint 계열의 도구들은 한번도 사용해본 적이 없는데 다른 것들도 한번 구해다 써봐야겠습니다. 이 툴의 유일한 문제는 비싸다는 것이랍니다. :-)