[글쓴이:] changwoo

  • 더 다양한 스터디

    현재까지 제안된 방법이 최선일까? 앞서 이야기하기도 했지만 절대 그렇지 않을 것이다. 더 다양한 방법이 있을 수 있을 것이다. 본 포스트에는 좀 더 나은 방법에 대해 기록하며, 그 이외의 다양한 이야기를 적어둘 것이다.

    콜백함수를 위한 템플릿을 이용한 2개의 클래스를 사용하는 것 보다 괜찮아 보이는 방법 중 하나는 ‘함수 어댑터(function adaptor)’를 사용하는 방법이다(출처).

    // binding function 제작
    #include <iostream>
    #include <cstddef>
    
    // 콜백 함수
    struct demo
    {
        void func() { std::cout << "called.n"; }
    };
    
    template <typename obj, void (obj::*xptr)() >
    struct func_bind
    {
        static obj x;
        static void call() { (x.*xptr)(); }
    };
    
    template <typename obj, void (obj::*xptr)(void) >
    obj func_bind::x;
    
    // 전역변수 콜백 함수 포인터.
    // 어떤 라이브러리 어딘가에 있다고 가정한다.
    void (*g_callback)(void) = NULL;
    
    int main(int argc, char** argv)
    {
        // 라이브러리에 우리의 콜백 함수를 등록하는 과정이라고 가정한다.
        func_bind<demo, &demo::func> bound;
    
        g_callback = bound.call;
        g_callback();    
    
        return EXIT_SUCCESS;
    }

     

    [그 외 이야기들]

    함수 어댑터

    멤버 함수를 위한 STL 함수 어댑터들이 있다.mem_fun_ref, mem_fun 들이 그것이다.

    #include <functional>
    
    demo d; // 멤버 변수를 가진 클래스
    std::mem_fun_t<void, demo>     p = std::mem_fun<void, demo>(&demo::func);
    std::mem_fun_ref_t<void, demo> r = std::mem_fun_ref<void, demo>(&demo::func);
    
    p(&d); // demo::func 멤버 함수 호출
    r(d);  // 위와 같으나 위는 객체의 포인터, 이것은 객체의 레퍼런스를 인자로 받는다.

    선언된 p나 r은 C-Style callback의 인자로는 사용할 수 없다.

    [GLUT Wrapper 등]

    • G. Stetten  과 K.Crawford는 GLUT의 C++ wrapper 인 GlutMaster를 만들었다. (홈페이지)
    • N. Stewart는 OpenGL C++ Toolkit를 공개했다. (홈페이지)

    [Function Object]

    • mem_fun, mem_fun_ptr 등은 functional 헤더에 정의되어 있다. 이 외에도 STL은 많은 함수 어댑터를 제공한다. 이들을 이용해 함수형 프로그래밍이 가능하다.
    • Boost library는 함수형 프로그래밍 뿐 아니라 C++ 프로그래밍에 있어 유용한 많은 기능을 제공한다. Boost에 대해서는 차후 따로 포스팅할 계획이다.

    차례로 돌아가기

  • GLUT Application

    GLUT의 callback 함수 매커니즘은 대표적인 C-style이고 인터넷 검색을 하면 많은 자료를 찾을 수 있다. 이는 나중에 다룰 생각이고, 이번에는 지금까지 만든 코드를 실제로 GLUT에 적용해 보도록 하겠다.

    본 예제는 M. Kilgard의 ‘A simple red cube drawn with minimal OpenGL calls.’를 바탕으로 변형한 것이다. 원본 소스는 여기서 다운로드 받을 수 있다.  GLUT는 freeglut을 사용하였다. 지금까지 차분히 따라왔다면 소스가 어떻게 구성되었는지 쉽게 감을 잡을 수 있을 것이다. 본 포스트에는 main 함수 부분만 간략하게 제시하고, 나머지 소스는 첨부 파일로 대신한다.

    #include <GL/freeglut.h>
    #include <iostream>
    #include "glut_callback.h"
    #include "box.h"
    
    int main(int argc, char** argv)
    {
        box my_box;    
    
        //box 초기화
        my_box.init_values();
    
        // 콜백될 때 함수 내부에서 사용될 클래스
        glut_callback::set_class(&my_box);
    
        // GLUT initialization code
        glutInit(&argc, argv);
        glClearColor(0.0, 0.0, 0.0, 0.0);
    
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
        glutCreateWindow("red 3D lighted cube");
    
        // glut main loop 이 멈추어도 나머지 코드를 실행하도록 만든다.
        glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_CONTINUE_EXECUTION);
        glutDisplayFunc(&glut_callback::draw_callback);
        glutKeyboardFunc(&glut_callback::keyboard_callback);
        glutIdleFunc(&glut_callback::idle_callback);
        glutMainLoop();
    
        return EXIT_SUCCESS;
    }

    다운로드: AdvancedCpp.zip

    이 예제는 cube 단 하나를 렌더링하므로 콜백 함수가 그다지 어렵지 않으며, 콜백 함수로 전달할 자료의 구조도 단순한 편이다. 그러나 여러 개의 오브젝트를 그려야 한다면? 나는 그냥 ‘scene graph’ 나 Orge3D 같은 간단한 게임 엔진을 사용하기를 권장한다…. 너무 무책임한가? 🙂

    차례로 돌아가기

  • Template Callback Class

    복잡한 캐스팅 콜백 함수 단계에서 일어나는 것을 방지하기 위해 템플릿을 도입해 보았다.

    #include <iostream>
    
    // 전역변수 콜백 함수 포인터.
    // 어떤 라이브러리 어딘가에 있다고 가정한다.
    void (*g_callback)() = NULL;
    
    // callback과 상속 관계가 없는 데이터 저장 클래스
    class storage
    {
    public:
        storage()  {}
        ~storage() {}
    
        int&       val()       { return val_; }
        const int& val() const { return val_; }
    
    private:
        int val_;
    };
    
    // 정적변수만을 취급하는 기본 클래스를 선언하였다.
    template <typename T>
    class callback_base
    {
    protected:
        callback_base() {}
        ~callback_base() {}
    
    public:
        static void set_class(T* _ptr)
        {
            T** base_ptr;
    
            base_ptr    = get_base_ptr();
            (*base_ptr) = _ptr;
        }
    
        // 이중 포인터 표현을 감출 수 있다.
        // 어떤 것을 사용하는지는 사실 사용자의 취향에 따라 달라질 수 있다.
        inline static T& get_class() { return **get_base_ptr(); }
    
    protected:
        // static 변수를 함수 안에 삽입하였다.
        // 그렇지만 엄연히 cb는 존재한다.
        static T** get_base_ptr()
        {
            static T* base;
            return &base;
        }
    };
    
    class callback_class : public callback_base<storage>
    {
    // cb_base의 직접적인 생성은 금지되어 있다.
    protected:
        callback_class() {}
        ~callback_class() {}
    
    public:
        // 라이브러리가 필요로 하는 콜백 함수
        // 상위 클래스에서만 스태틱 변수를 관리하고
        // 상위 클래스를 상속받아 콜백 함수를 새롭게 정의한다.
        // storage 클래스는 사용자가 직접 만들어야 할 변수이다.
        static void callback()
        {
            // 멤버 변수를 조작할 수 있도록 클래스를 얻는다.
            // 구질구질한 포인터 참조 연산자가 사라졌다.
            storage&  real = get_class();
    
            std::cout << "member variable n: " << real.val() << std::endl;
        }
    };
    
    int main(int argc, char** argv)
    {
        // 일반적인 데이터 저장 클래스이다.
        // 복수 개를 선언하여 여러 콜백이 동시에 동작할 수 있는지 확인한다.
        storage s1, s2;
    
        s1.val() = 10;
        s2.val() = 20;
    
        // 같은 static 함수를 callback하되
        g_callback = &callback_class::callback;
    
        // 때때로 다른 데이터를 선택하도록 한다.
        // 템플릿 인자로 데이터 타입이 명시되어 가독성이 높아지는 부가적인 효과를 얻었다.
        callback_class::set_class(&s1);
        g_callback();    
    
        callback_class::set_class(&s2);
        g_callback(); 
    
        return EXIT_SUCCESS;
    }

    템플릿을 사용하여 클래스를 사용할 수 있는 콜백 형태를 만들었다.  하지만 여전히 몇몇 문제가 남아 있을 것이다. 그 중 “다양한 데이터 클래스들의 콜백 함수를 다룰 때는 그 다양한 인자를 어떻게 콜백 함수로 전달할 것인가?”라는 문제는 직감적으로 다가온다.

    일단, 내 기본적인 아이디어는 콜백 함수에서 스태틱 포인터 변수를 활용하여 데이터 관리 객체의 비정적 멤버 변수에 접근하는 방법을 제안하는 것이며, 실제로 이 아이디어는 나 혼자만의 아이디어는 아님을 밝혀 둔다. 또한 이 방법이 최선이라고는 말할 수 없다. 더 훌륭한 방법들이 존재할 것이다.

    어쨌든 C-Style의 콜백 함수가 그다지 C++에 걸맞는 구조가 아님에도 불구하고,
    이런 방법을 통해 콜백 함수에서 클래스를 사용할 수 있다는 것을 제안함에 의의를 둔다.

    참조:
    CPUBitmap Class, J. Sanders, E. Kandrot, ‘CUDA by Example’, Addison Wesley.

    차례로 돌아가기

  • Data Class and Callback Class

    스태틱 클래스 함수라도 자기 자신을 참조하는 스태틱 클래스 변수에 힘입어 자신의 비스태틱 멤버 변수 및 함수에 접근할 수 있게 되었다. 그러나 콜백 함수 내부에서 접근 제한이 불가능한 문제를 발견할 수 있었다. 이러한 문제는 데이터/콜백 클래스를 각기 완전히 분리를 하면 해결할 수 있다.

    #include <iostream>
    
    // 전역변수 콜백 함수 포인터.
    // 어떤 라이브러리 어딘가에 있다고 가정한다.
    void (*g_callback)() = NULL;
    
    // callback과 상속 관계가 없는 데이터 저장 클래스를 만든다.
    class storage
    {
    public:
        storage()  {}
        ~storage() {}
    
        int&       val()       { return val_; }
        const int& val() const { return val_; }
    
    private:
        int val_;
    };
    
    // 정적변수만을 취급하는 기본 클래스를 선언하였다.
    class callback_base
    {
    protected:
        callback_base() {}
        ~callback_base() {}
    
    public:
        static void set_class(void* _ptr)
        {
            void** base_ptr;
    
            base_ptr    = get_base_ptr();
            (*base_ptr) = _ptr;
        }
    
    protected:
        // static 변수를 함수 안에 삽입하였다.
        // 그렇지만 엄연히 cb는 존재한다.
        static void** get_base_ptr()
        {
            static void* base;
            return &base;
        }
    };
    
    // 콜백 함수만을 정의할 수 있도록 콜백 기본 클래스를 상속받는다.
    class callback_class : public callback_base
    {
    // cb_base의 직접적인 생성은 금지되어 있다.
    protected:
        callback_class() {}
        ~callback_class() {}
    
    public:
        // 라이브러리가 필요로 하는 콜백 함수
        // 상위 클래스에서만 스태틱 변수를 관리하고
        // 상위 클래스를 상속받아 콜백 함수를 새롭게 정의한다.
        // storage 클래스는 사용자가 직접 만들어야 할 변수이다.
        static void callback()
        {
            // 멤버 변수를 조작할 수 있도록 클래스를 얻는다.
            storage&  real = *(storage*)(*get_base_ptr());
    
            std::cout << "member variable n: " << real.val() << std::endl;
        }
    };
    
    int main(int argc, char** argv)
    {
        // 일반적인 데이터 저장 클래스이다.
        // 복수 개를 선언하여 여러 콜백이 동시에 동작할 수 있는지 확인한다.
        storage s1, s2;
    
        s1.val() = 10;
        s2.val() = 20;
    
        // 같은 static 함수를 callback하되
        g_callback = &callback_class::callback;
    
        // 때때로 다른 데이터를 선택하도록 한다.
        callback_class::set_class(&s1);
        g_callback();    
    
        callback_class::set_class(&s2);
        g_callback(); 
    
        return EXIT_SUCCESS;
    }

    멤버 변수로 존재하던 정적 변수를 정적 함수 내부로 집어 넣었다. 콜백 클리스와 데이터 클래스를 분리시킴으로써 콜백이 보다 구조적인 형태를 갖추었다. 실제로 콜백을 구현할 때에는 콜백의 기본 클래스를 상속받아  라이브러리가 필요한 형태의 콜백 함수를 정적 함수 형태로 구현하면 된다.

    그러나 아직 마음에 걸리는 것이 있다.  바로 콜백 부모 클래스의 ‘void *’가 그것이다. 이것이 콜백함수를 구현하는 중에 복잡한 캐스팅을 일으키게 한다. 이를 없애는 방법은 없을까? 나는 템플릿이 해결해줄 수 있을 것이라 생각한다..

    차례로 돌아가기

  • Static Variable

    일반적인 클래스로는 c-style의 콜백을 처리할 수 없다는 것을 알았고, 이는 단순한 대처로는해결하기 힘듬을 알았다. 그렇다면 어떻게 클래스를 사용하면서도, c-style을 유지할 수 있을까?

    한 가지 방법으로는 static 변수를 멤버 변수로 사용하는 방법이다. 그러나 이 방법은 그리 유용하지 못하다. 왜냐하면 static 멤버 변수는 클래스를 선언하지 않고도 사용 가능하며, 그 클래스가 모두 공유하는 변수이기 때문에, 사실상 전역 변수이나 다름이 없어지기 때문이다.

    우리는 클래스의 멤버 변수가 코드 안에서 자유롭게 선언되기를 (정확하게는 스택/힙 영역에서 메모리 확보가 이루어지기를) 원한다.  한가지 기본적인 대안은, 콜백 함수 내에서 자신이 속한 클래스의 레퍼런스를 가지고 있다가 콜백 함수에서 사용하는 것이다. 이 방법은 나름 설득력이 있어 보인다.

    #include <iostream>
    
    // 전역변수 콜백 함수 포인터.
    // 어떤 라이브러리 어딘가에 있다고 가정한다.
    void (*g_callback)(void) = NULL;
    
    // 우리는 C-Style의 함수가 아닌 class를 필요로 한다.
    class cb_class
    {
    public:
         cb_class() : n(100) {}
        ~cb_class() {}
    
        // 라이브러리는 이 함수를 필요로 한다.
        // static 키워드를 포함할 때와 그렇지 않을 때를 비교하라.
        static void callback(void)
        {
            cb_class& real = *ptr;
    
            std::cout << "Our callback function is called.n";        
    
            std::cout << "member variable n: " << real.n << std::endl;
        }
    
        static cb_class* ptr;
    
    private:
        int n;
    };
    
    cb_class* cb_class::ptr = NULL;
    
    int main(int argc, char** argv)
    {
        // 라이브러리에 우리의 콜백 함수를 등록하는 과정이라고 가정한다.
        g_callback = &cb_class::callback;
    
        // 일반적인 콜백 클래스를 선언한다.
        cb_class cb;
        cb_class::ptr = &cb;
    
        // g_callback이 우리가 만든 callback을 대신한다.
        // 현재 우리가 만든 main 함수 안에서 호출되고 있으나
        // 이 while문의 구조는 라이브러리 내부 어딘가라고 가정한다.
        int nCount = 3; // 3회 callback 함수를 호출
        while(nCount--)
        {
            g_callback();
        }
    
        return EXIT_SUCCESS;
    }

    가장 단순한 형태이지만, 이것은 콜백을 수행하는 스태틱 함수에서 멤버 변수를 사용할 수 있는 방법이 될 수 있다. 그러나 콜백함수의 인자를 넣고 참조하는 방법의 가독성은 좋지 않을 뿐더러, 콜백 함수에서 클래스의 멤버에 아무 제한 없이 접근할 수 있다는 점은 문제로 남아 있다. 이러한 문제는 콜백 클래스와 데이터 클래스를 분리함으로써 해결할 수 있다.

    차례로 돌아가기

  • Callback and Class

    C-Style의 callback 코드에 살짝 C++ 개념을 얹어 보자.

    #include <iostream>
    
    // 전역변수 콜백 함수 포인터.
    // 어떤 라이브러리 어딘가에 있다고 가정한다.
    void (*g_callback)(void) = NULL;
    
    // 우리는 C-Style의 함수가 아닌 class를 필요로 한다.
    // C++에서 class와 struct의 차이는 단지 private이 기본이냐, public이 기본이냐의 차이다.
    struct cb_class
    {
        // 라이브러리는 이 함수를 필요로 한다.
        // static 키워드를 포함할 때와 그렇지 않을 때를 비교하라.
        static void callback(void)
        {
            std::cout << "Our callback function is called.n";
        }
    };
    
    int main(int argc, char** argv)
    {
        // 라이브러리에 우리의 콜백 함수를 등록하는 과정이라고 가정한다.
        g_callback = &cb_class::callback;
    
        // g_callback이 우리가 만든 callback을 대신한다.
        // 현재 우리가 만든 main 함수 안에서 호출되고 있으나
        // 이 while문의 구조는 라이브러리 내부 어딘가라고 가정한다.
        int nCount = 3; // 3회 callback 함수를 호출
        while(nCount--)
        {
            g_callback();
        }
    
        return EXIT_SUCCESS;
    }

    static 키워드를 붙이지 않았을 경우,
    error C2440: ‘=’ : cannot convert from ‘void (__thiscall cb_class::* )(void)’ to ‘void (__cdecl *)(void)’
    에러를 내며 컴파일이 올바르게 수행되지 않는다.
    클래스의 멤버 함수는 C-Style의 콜백 함수로써 사용이 불가능하며, 굳이 사용해야 할 경우 반드시 ‘정적 멤버 함수’로 만들어야 한다. 이로써 문제점이 명확히 제시되었다.

    “우리는 어떻게 해서든 클래스의 멤버 변수를 사용하고 싶다!”

    차례로 돌아가기