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

std::map에 insert하기

얼마전 회사 동료가 refactoring한 코드를 열심히 revert하고 있어서 물어보니 다음과 같은 문제였습니다. 원래 코드와 refactoring한 코드는 다음과 같더군요. nvp[name] = value; // original code nvp.insert(make_pair(name, value)); // refactored 아시겠지만 위의 두 라인은 전혀 다른 기능을 하죠. C++03에 보면 각각 다음과 같이 설명되어 있습니다. 23.1.2/7 Associative containers a_uniq.insert(t): pair<iterator, bool> inserts t if and only if there is no element in the container with key equivalent to the key of t. The bool component of the returned pair indicates whether the insertion takes place and the iterator component of the pair points to the element with key equivalent to the key of t. 23.3.1.2/1 map element access [lib.map.access] T& operator[](const key_type& x); Returns: (*((insert(make_pair(x, T()))).first)).second. 원래 코드는 매번 새 값으로 이전 값을 overwrite했지만 새 코드는 이전에 키가 존재하면 새값으로 overwrite하지 않습니다. 따라서 원래 기능이 제대로 동작하지 않게 된것이죠. 그래서 물어봤죠. "왜 이렇게 했어?" "insert가 성능이 더 좋다 그래서 했지." :-? 사실 Fowler 아저씨는 Refactoring 책에서 refactoring은 성능을 optimizing하기 위한 것이 아니다라

C++ of the Day #9 - Boost.Python 사용하기 #1

Python 은 가장 인기있는 interpret 언어중의 하나입니다. Python의 장점 중 하나는 C/C++ 모듈과 쉽게 연동할 수 있다는 점입니다. 물론 손으로 일일히 wrapper를 만드는 것은 손이 많이 가고 에러를 만들수 있는 작업이나 SWIG 등과 같은 도구를 사용하면 쉽게 python 모듈을 만들 수 있습니다. Boost.Python 은 이런 SWIG와 같이 python 모듈을 쉽게 만들 수 있도록 도와주는 라이브러리로 순수 C++만을 사용한다는 점이 SWIG와 다른 점입니다. 그리고 개인적으로는 Boost 라이브러리에 포함되어 있는 것들이 왠지 좀 더 믿음직스러워서... :-) 이번 글에서는 Boost.Python 문서에 나와 있는 예제 를 가지고 간단하게 python 모듈을 만드는 방법에 대해서 알아보겠습니다. Requirements 리눅스 이 글에서는 리눅스 환경에서의 사용 방법을 설명한다. Boost.Python 라이브러리 (1.33.1) Boost 라이브러리를 다운로드받아 아래와 유사한 명령으로 라이브러리를 빌드한다. bjam -sTOOLS=gcc -with-python install bjam의 --prefix 옵션으로 라이브러리가 설치될 위치를 변경할 수 있다. Python 라이브러리 (2.4.3) Python을 다운로드 받아 빌드하여 설치한다. 위의 경우와 유사하게 configure의 --prefix 옵션으로 설치될 위치를 변경할 수 있다. Write C++ Code 다음과 같이 코드를 작성한다. // greet.cpp #include <stdexcept> char const* greet(unsigned x) { static char const* const msgs[] = { "hello", "Boost.Python", "world!" }; if (x > 2) throw std::range_error("