이원구님의 C++ of the Day #33 – dynamic visitor를 읽다가 문득 생각이 났다. Visitor를 generic하게 구현할 수 있지 않을까???
그래서 했다.
다운로드: generic_visitor.cpp
핵심 클래스는 visit_unit이다.
template<class Visitable> struct visit_unit { public: typedef typename boost::function<void (Visitable&)> visit_function; void register_visitor(visit_function func){...} void visit(Visitable& visitable){...} }; struct default_generic_visitor { public: void register_visitor(){} void visit(){} };
대략 위와 같은 구조를 하고 있는데, visit_unit은 Visitable클래스에 대한 함수자를 vector로 관리해주는 녀석이고, default_generic_visitor는 연쇄상속기법을 사용하기 위해 만든 마지막 Tail클래스다.
연쇄상속기법을 이용하는 코드는 다음과 같다. (ㄷㄷㄷ)
template<class visitable_list> struct generic_visitor_ : public boost::mpl::if_c< boost::mpl::size<visitable_list>::type::value != 1, generic_visitor_<typename boost::mpl::pop_front<visitable_list>::type>, default_generic_visitor >::type , public visit_unit<typename boost::mpl::front<visitable_list>::type> { using visit_unit<typename boost::mpl::front<visitable_list>::type>::visit; typedef typename boost::mpl::if_c< boost::mpl::size<visitable_list>::type::value != 1, generic_visitor_<typename boost::mpl::pop_front<visitable_list>::type>, default_generic_visitor >::type parent; // 다중상속 관계때문에 함수를 빌려와야한다. using parent::visit; };
boost::mpl을 많이 빌려왔다.. 🙂 generic_visitor_는 일종의 구현부인데, 자신이 보고있는 visitable_list의 크기가 1이면 default_generic_visitor를 상속받고, 아니면 visitable_list의 제일 앞놈을 뺀 generic_visitor_를 상속받는다. 또한, visit()등의 함수를 실제로 구현하는 visit_unit을 별도로 상속받는다.
그리고, generic_visit_에 의해 생성된 상속트리를 generic_visitor가 상속받는다.
template<class visitable_list> struct generic_visitor : generic_visitor_<visitable_list> { template<class T, class F> void register_visitor(F f) { boost::function<void (T& t)> func(f); static_cast<visit_unit<t>*>(this)->register_visitor(f); } };
이녀석은 register_visitor의 템플릿 버젼을 구현하고 있다. 요렇게 static_cast를 해주면 해당하는 녀석을 매우 잘 찾아간다.
이렇게 이야기하면 이해가 안갈거다. 그래서 그림으로 그려보기로 했다.
먼저, visitable_list를 (AClass, BClass, CClass, DClass)로 표기한다. 화살표를 쏘는 쪽이 부모다.
default_generic_visitor
↓
generic_visitor_<visitable_list = (DClass)> <- visit_unit
↓
generic_visitor_<visitable_list = (CClass, DClass)> <- visit_unit
↓
generic_visitor_<visitable_list = (BClass, CClass, DClass)> <- visit_unit
↓
generic_visitor_<visitable_list = (AClass, BClass, CClass, DClass)> <- visit_unit
↓
generic_visitor<visitable_list = (AClass, BClass, CClass, DClass)>
대략 이런식의 상속관계가 된다.
해당 클래스에 대한 함수등록 메소드인 register_visitor나 실제 함수 호출을 해주는 visit 메소드는 visit_unit 클래스 템플릿에 구현이 되어있으며, 나머지는 저 상속관계를 자동으로 만들어주기 위한 코드일 뿐이다.
실제로 쓰려면 다음과 같다.
void visit_a(AClass& a) { std::cout << "visit_a()" << std::endl; } void visit_b(BClass& b) { std::cout << "visit_b()" << std::endl; } void visit_c(CClass& b) { std::cout << "visit_c()" << std::endl; } int _tmain(int argc, _TCHAR* argv[]) { AClass a; BClass b; CClass c; Base base; typedef boost::mpl::vector<aclass, BClass, CClass> type_vector; generic_visitor<type_vector> gv; boost::function< void (AClass&) > vf(&visit_a); gv.register_visitor<aclass>(vf); // visit함수 등록! gv.register_visitor<bclass>(&visit_b); // visit함수 등록! gv.register_visitor<cclass>(&visit_c); // visit함수 등록! gv.visit(a); // visit_a()가 콘솔에 출력되어야함 gv.visit(b); // visit_b()가 콘솔에 출력되어야함 gv.visit(c); // visit_c()가 콘솔에 출력되어야함 return 0; }
boost::function을 함수타입으로 쓰고 있으므로, boost::bind를 하건, operator()를 써서 만들건, C타입의 함수이건 멤버함수이건 상관없이 쓸 수 있다! 게다가, 서로 다른 클래스에 있는 녀석들도 한데 묶어서 한 Visitor로 처리할 수도 있고, 한 타입에 대해 여러 visit함수를 사용할수도 있다!
실은, 컴파일 타임에 visit함수자를 반드시 등록하도록 강제하고 싶었지만, 그렇게 할 경우 유연성이 너무 떨어질 것 같아서 하지 못했다. 아참. 그리고, 이 방법의 문제점은 type_vector에 있는 클래스가 너무 많을 경우, 컴파일러가 죽을수도 있다. (대충 50개 이상?) 타입 이름이 너무 길고 구조가 복잡하면, MSVC 컴파일러는 종종 죽어버린다.. 🙁
+1. 사실 아이디어는 간단한데 구현하느라 삽질을 좀 했다. template function를 쓰면 간단하긴 한데, 문제는 template function은 바인드가 잘 안된다.. GG
+2. mpl::fold를 써서 후다닥 하려고 했지만, using을 쓰는 등의 문제점이 있어서 포기.
잘 봤습니다. ^^
저도 dynamic visitor 2 만들면서는 비슷한 방법을 사용했는데 저는 mpl::inherit_linearly 를 사용했습니다.
그리고 저는 함수 포인터 – Ret (*)(T*) – 를 사용했는데 boost::function을 사용하셔서 더 범용적인것 같네요.
그런데 generic_visitor_ 랑 visit_unit이 굳이 따로 있을 필요가 없을 것 같기도 하네요.
그리고 mpl을 과다하게 쓰다 보면 g++도 가끔 죽어요. >.
mpl::inherit_linearly가 있는줄 알았으면 사용했을텐데.. 미처 몰랐네요.. T_T
그걸 알았다면.. visit_unit을 사용할 필요도 없었을텐데.. 다시 만들어봐야겠네요.
g++도 죽는군요. 허허. (조만간 g++과 살아야할듯.. 하아)
mpl::inherit_linearly를 사용해 보았는데.. 역시나 using parent::visit부분이 문제가 되는군요.
generic_visitor 클래스에 template void visit(T& t){}와 같은 함수를 만들고 여기서 static_cast*>(this); 를 해주면 되긴 하지만, 이렇게 할 경우 boost::bind가 잘 되지 않는다는 문제가 있습니다.
굉장히 지저분하지만 어쩔수 없는걸까요.. 하아.
아니면 mpl::inherit를 상속받아서 처리하는 방법이 있을지도.. 음. (실험중입니다. ^^)