본문 바로가기

진리는어디에

[C++] C++에서 C# delegate 만들기

Delegate란?

사전적 의미의 delegate는 '대리자'로써 뭔가를 대신 해준다는 의미고, C#에서의 delegate는 일종의 콜백 함수'리스트'로써 입력과 출력이 동일한 함수들을 일괄 호출하는데 사용된다.

이게 C++ 관점에서 보면 함수 포인터 리스트를 들고 있는 간단한 클래스 정도인데, 개념적으로도 어렵지 않고 구현하는 것도 그렇게 어렵지 않다. 말 그대로 함수 포인터 리스트를 만들어도 되고, 인터페이스 클래스를 정의하고 그걸 상속 받은 클래스 리스트를 만들어도 된다. 다만...필요할때 마다 매번 만드는게 은근 귀찮아 template과 operator overriding을 이용하여 재사용 가능한 delegate 클래스를 만들어 보도록 하겠다(c++ 11 이상).

C# Delegate의 인터페이스

  • 콜백 함수를 등록하기 위한 operator '+='
  • 등록된 콜백 함수를 제거하기 위한 operator '-='
  • 등록된 콜백 함수를 개별로 호출 하기 위한 iterate 인터페이스
  • Delegate는 글로벌 함수, 클래스 멤버 함수, 람다를 등록 할 수 있어야 한다.

정도로 정리가 된다.

Delegate.h

사용하다 보니 Delegate에서 리턴 값을 사용하는 경우가 극히 드물고(필자의 경우는 한번도 없었다), 결정적으로 등록된 함수들로 부터의 리턴 값들을 어떻게 처리 할지가 애매했다. 리턴 값들을 다 더할 수도 없고, 곱할 수도 없고, 비트 연산을 할 수도 없고...그래서 리턴 값을 가지는 함수는 Delegate에 등록하지 못하도록 변경 했다.
또한 글로벌 함수 외에는 제대로 삭제가 되지 않는 버그가 있어 수정 했다. 위에 링크된 코드는 히스토리 유지상 남겨 두는 것이고, 본문에 있는 아래 코드를 보도록 하자.

// Delegate.h
#ifndef _DELEGATE_H_
#define _DELEGATE_H_

#include <list>
#include <functional>

template <class... ARGS>
class Delegate
{
public :
    typedef typename std::list<std::function<void(ARGS...)>>::iterator iterator;
    
    void operator () (const ARGS... args)
    {
        for (auto& func : functions)
        {
            func(args...);
        }
    }
    
    Delegate& operator += (std::function<void(ARGS...)> const& func)
    {
        functions.push_back(func);
        return *this;
    }
    
    Delegate& operator -= (std::function<void(ARGS...)> const& func)
    {
        void (* const* func_ptr)(ARGS...) = func.template target<void(*)(ARGS...)>();
        const std::size_t func_hash = func.target_type().hash_code();
        
        if (nullptr == func_ptr)
        {
            for (auto itr = functions.begin(); itr != functions.end(); itr++)
            {
                if(func_hash == (*itr).target_type().hash_code())
                {
                    functions.erase(itr);
                    return *this;
                }
            }
        }
        else
        {
            for (auto itr = functions.begin(); itr != functions.end(); itr++)
            {
                void (* const* delegate_ptr)(ARGS...) = (*itr).template target<void(*)(ARGS...)>();
                if (nullptr != delegate_ptr && *func_ptr == *delegate_ptr)
                {
                    functions.erase(itr);
                    return *this;
                }
            }
        }
        return *this;
    }

    iterator begin() noexcept
    {
        return functions.begin();
    }
    iterator end() noexcept
    {
        return functions.end();
    }
    void clear()
    {
        functions.clear();
    }

private :
    std::list<std::function<void(ARGS...)>> functions;
};

#endif
  • 24 ~ 54 라인 : 등록 된 함수 포인터의 주소를 비교하여 리스트에서 삭제해주는 오퍼레이터다.
    26 ~ 27 라인에서 삭제 하려는 함수의 해쉬 코드 값함수 포인터를 얻어온다. 여기서 주목 해야 할 부분은, 클래스 멤버 함수나, 람다의 경우는 <void(*)(ARGS...)> 타입이 아니기 때문에 target을 이용해 함수 포인터를 얻어 오려고 하면 nullptr을 리턴한다. 반대로 일반 함수의 경우는 target_type을 이용해 해쉬 코드를 얻어 오면 같은 포멧의 함수는 같은 해쉬 코드를 리턴하기 때문에 구분이 불가능 하다. 그래서 위 코드의 29 ~ 52 라인 처럼 일반 함수와 다른 타입의 함수들을 구분 해서 처리 해줘야 한다.
  • 26, 45 라인 : 각 라인들을 보면 func.template target<...> 처럼 익숙하지 않은 문법이 보인다. std::function::target 템플릿 멤버 함수를 사용 할 때, 템플릿 클래스의 템플릿 멤버 함수를 호출하게 되면 컴파일러가 타입을 제대로 추론하지 못해 템플릿 멤버 함수에 대해서 컴파일 오류를 발생 시킨다. 때문에 컴파일러에게 명시적으로 템플릿 함수라는 것을 알려 주기 위해 함수의 호출 부 앞에 'template' 키워드를 붙여 주었다.
  • 56 ~ 67 라인 : 임의 iteration을 위한 iterator 인터페이스.
  • 70 라인 : 일반 함수 뿐 아니라, 클래스 멤버 함수, 람다도 저장 할 수 있도록 std::function 사용
반응형

Example

아래는 Delegate 클래스의 사용예를 보여주는 코드다. 참고로 람다는 선언 될 때 마다 타입의 해쉬 값이 달라지므로 등록한 람다를 추후에 삭제하기 위해서는 아래 예제 처럼 함수 객체를 저장하고 있어야 한다.

#include <string>
#include <iostream>
#include "Delegate.h"

void foo(const std::string& msg)
{
    std::cout << "\tfunction foo:" << msg << std::endl;
}

void bar(const std::string& msg)
{
    std::cout << "\tfunction bar:" << msg << std::endl;
}

class Foo
{
public :
    void member_function(const std::string& msg)
    {
        std::cout << "\tmember function of Foo class:" << msg << std::endl;
    }
};

class Bar
{
public:
    void member_function(const std::string& msg)
    {
        std::cout << "\tmember function of Bar class:" << msg << std::endl;
    }
};

int main()
{
    Foo fooObj;
    Bar barObj;
    
    Delegate<const std::string&> delegate;
    // Delegate<> delegate; 템플릿 인자가 없는경우
    // Delegate delegate; C++17 부터는 인자가 없으면 아예 빼도 됨
    
    std::cout << "// add 'foo' function" << std::endl;
    delegate += foo;
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// add 'bar' function" << std::endl;
    delegate += bar;
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// add 'Foo::member_function' function" << std::endl;
    delegate += std::bind(&Foo::member_function, &fooObj, std::placeholders::_1);
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// add 'Bar::member_function' function" << std::endl;
    delegate += std::bind(&Bar::member_function, &barObj, std::placeholders::_1);
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// remove 'Bar::member_function' function" << std::endl;
    delegate -= std::bind(&Bar::member_function, &barObj, std::placeholders::_1);
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// remove 'foo' function" << std::endl;
    delegate -= foo;
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// remove 'bar' function" << std::endl;
    delegate -= bar;
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// remove 'Foo::member_function' function" << std::endl;
    delegate -= std::bind(&Foo::member_function, &fooObj, std::placeholders::_1);
    delegate("Hello World");
    std::cout << std::endl;
    
    auto lambda = [](const std::string& msg) {
        std::cout << "\tlambda:" << msg << std::endl;
    };
    std::cout << "// add 'lambda' function" << std::endl;
    delegate += lambda;
    delegate("Hello World");
    std::cout << std::endl;
    
    std::cout << "// remove 'lambda' function" << std::endl;
    delegate -= lambda;
    delegate("Hello World");
    std::cout << std::endl;
}

결과

// add 'foo' function
        function foo:Hello World

// add 'bar' function
        function foo:Hello World
        function bar:Hello World

// add 'Foo::member_function' function
        function foo:Hello World
        function bar:Hello World
        member function of Foo class:Hello World

// add 'Bar::member_function' function
        function foo:Hello World
        function bar:Hello World
        member function of Foo class:Hello World
        member function of Bar class:Hello World

// remove 'Bar::member_function' function
        function foo:Hello World
        function bar:Hello World
        member function of Foo class:Hello World

// remove 'foo' function
        function bar:Hello World
        member function of Foo class:Hello World

// remove 'bar' function
        member function of Foo class:Hello World

// remove 'Foo::member_function' function

// add 'lambda' function
        lambda:Hello World

// remove 'lambda' function

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!