Delegate는 대리자라는 뜻을 가지고 있습니다. C#에 있는 기능인데 리턴값이나 인자가 같은 함수, 멤버 함수, 람다, functor 등을 종류에 상관없이 서로 일관된 방식으로 호출할 수 있게 해줍니다. C++에선 std::function으로 이런 기능을 사용할 수 있지만, 아래 소개할 몇 가지 단점 때문에 새로 만들어 보기로 합니다.
개요
함수, functor, 클래스 메서드와 같이 호출 가능한 것들을 통일된 방식으로 다루기 위해선 delegate가 필요합니다. C++ STL의 <functional>헤더에 std::function를 사용할 수 있지만 다음과 같은 단점을 가지고 있습니다.
- 기본 크기가 크다 – std::function은 할당 오버헤드를 줄이기 위해 크기가 작은 callable객체들은 스택에 저장하고 큰 객체들은 동적할당을 통해 힙에 저장하는 방식을 취하고 있습니다. 하지만, 메모리를 거의 잡아먹지 않는 callable 객체(static 함수, 멤버함수, non capturing 람다 등)을 담는 std::function은 사용하지 않는 메모리가 더 많습니다. 구체적으론 컴파일러 마다 다르겠지만 MSVC에선 x86에서 40바이트, x64에서 64바이트를 소모합니다.
- 멤버 함수를 호출할 수 없다 – 사실 std::function을 통해 멤버함수를 호출하는 방법은 존재합니다. std::bind를 사용하거나 람다로 인스턴스를 참조로 캡쳐하는 방법이 있습니다. 하지만 두 방법 모두 미관상 보기좋은 코드를 만들긴 힘듭니다.
- 서로 비교할 수 없다 – std::function은 nullptr과의 비교, 담고 있는 함수 포인터의 반환을 지원합니다. 만약 위에 소개된 방식으로 멤버함수를 넘겨줬다면 그 인스턴스를 알아내기가 쉽지 않습니다.
이 중에서 크기가 크고 비교가 안된다는 단점이 크게 다가와서 이번에 만들어 볼 delegate는 메모리와 호출 성능, 멤버 함수의 바인딩을 우선순위로 둡니다. 먼저 delegate를 구현하기 위해 C++에서 호출 가능한 것들을 분류해 봅시다.
C++에서 존재할 수 있는 호출 가능한 것들의 종류는 다양합니다.
- 전역 함수
- static 멤버함수
- 멤버함수
- const 멤버함수
- functor
- 람다
구체적으로 CV qualifier라던가 호출 규약을 따진다면 사실 이것보다 더 많습니다. 얘네들을 구현상 편하게 하기 위해서 분류를 해봅시다.

먼저 정적(static)이냐 비정적이냐로 우선적으로 분류할 수 있습니다. 일반적인 함수, static 멤버 함수, non capturing lambda등이 static에 해당하고 멤버 함수, capturing lambda, functor 등과 같이 인스턴스가 필요한 것들이 non static에 해당합니다.

그 다음 비정적 함수에선 객체를 소유하고 있는 대상이 무엇인가에 따라 또다시 분류합니다. lambda나 functor는 상태를 delegate가 내부적으로 소유하고 있어야 하기 때문에 stateful이라고 구분합니다. (비정적) 멤버 함수는 소유권이 delegate에 있지 않은 외부의 인스턴스를 참조하기 때문에 instance라고 구분합니다. 이 두 구분이 중요한 이유는 stateful의 경우에 delegate 내부적으로 메모리를 관리해줘야 하기 때문입니다.
마지막으로 객체 지향 개념인 상속, 가상 상속, 다중 상속, 다형성 때문에 비정적 멤버 함수에 대해선 한번 더 분류가 필요합니다.

모든 종류의 멤버 함수를 delegate에 바인딩 하기 위해선 멤버 함수 포인터의 성질에 대해 알아야 합니다. static 멤버 함수의 함수 포인터는 일반적인 포인터의 크기와 동일하고 void* 심지어 정수형으로 형변환이 가능합니다. 실제로 내부적으로 일반적인 함수와 동일하게 동작합니다. 하지만 멤버 함수 포인터는 어떤 클래스냐에 따라 그 크기가 변합니다. 또 reinterpret_cast를 사용해도 일반 포인터 형으로 형 변환이 불가능 합니다. 문제는 컴파일러마다 크기가 또 다르다는 겁니다. 참조 1에서 이 문제를 해결하기 위해 서로 크기가 다른 멤버 함수 포인터를 정규화 시키는 방법을 소개하고 있는데 여기서 그 방법을 사용해 보도록 합니다.
가상함수에 관해서 더 최적화 할 수 있는 부분이 있습니다. C++에서 가상함수의 포인터는 동적 바인딩이 작동하도록 만들어졌습니다. 하지만 delegate는 생성할 때부터 인스턴스가 이미 결정되어 있기 때문에 가상함수의 동적 바인딩은 필요 없습니다. 따라서 가상 함수 테이블을 풀어내서 성능을 향상하는 방법도 사용해 보겠습니다.
구현
구현에 앞서 64바이트 기준으로 설명한다는 점 알려드립니다. 먼저 delegate에 필요한 데이터의 최소 크기를 알아봅시다. 위에서 분류한 static에 해당하는 것들만 바인딩 될 수 있다면 함수포인터만 저장하면 되기 때문에 8바이트만 저장하면 됩니다. 하지만, non static을 바인딩 하려면 함수 포인터에 인스턴스의 포인터로 8바이트가 추가로 필요합니다. 또 stateful을 바인딩 하려면 객체를 저장하기 위해 동적할당이 필요합니다. 하지만 동적할당을 사용한다면 delegate가 소멸할 때 stateful 객체를 감지하고 적절하게 해제할 방법이 필요합니다. 따라서 이러한 stateful이 저장되었다는 것을 확인하기 위해 추가로 바이트가 필요하지만… 없어도 되도록 구현했습니다. 따라서 총 16바이트가 필요합니다. x64의 경우 포인터의 크기는 8바이트 이지만, 8바이트를 다 쓰는 주소는 사실상 없기 때문에 쓰지 않는 비트를 날려서 12바이트에 저장하는 방법도 있습니다. 하지만, 메모리 정렬 문제와 구현상의 편의 때문에 사용하지 않겠습니다.
먼저 구현상 가장 일반적인 경우인 single inheritance non virtual(일반적인 멤버 함수)의 바인딩부터 살펴봅시다.
class A {
public:
void member_func(int a, int b, int c); // -> void member_func(A* this, int a, int b, int c);
};
member_func는 인자를 3개 받고 리턴값이 없는 함수 시그니처를 가지지만 실제론 4개의 인자를 가집니다. 왜냐하면 인자의 맨 앞에 인스턴스의 포인터인 this를 따로 받기 때문입니다. 컴파일러가 자동으로 이 부분에 알맞은 인스턴스의 포인터를 넘겨주기 때문에 맨 앞의 인자인 this는 숨겨집니다.
// member of delegate
DEL_INLINE Ret operator()(Args... args) const {
return (closure.get_inst()->*(closure.get_fptr()))(std::forward<Args>(args)...);
}
delegate에서 실제로 함수를 호출하는 부분입니다. 보시면 ->* 연산자로 저장해놓은 인스턴스를 멤버 함수 포인터로 호출하는 것을 볼 수 있습니다. 일반적인 멤버함수는 이렇게 간단하게 호출할 수 있습니다. 이제 가상함수를 호출하는 방법을 알아봅시다.
가상함수도 일반적인 멤버함수처럼 인스턴스와 멤버 함수 포인터로 바로 호출할 수 있습니다. 하지만 위에서 언급했듯이 일반 멤버 함수처럼 다루게 되면 동적 바인딩이 작동하기 때문에 호출 오버헤드가 더 발생하게 됩니다. 따라서 실제로 호출되는 함수 주소를 풀어낸 뒤 호출하면 불필요한 메모리 접근이 줄어들기 때문에 더 효율적이게 됩니다. 문제는 실제로 호출해야 될 함수 포인터를 얻는 과정이 표준에서 벗어난다는 것입니다.
#if defined(DEL_USES_GCC)
template <class Virtual_ptr>
ptrdiff_t _virtual_indexof(Virtual_ptr vfptr) { // for gcc
typedef struct {
void** functionPtr;
size_t offset;
} MP;
MP& t = *(MP*)&vfptr;
if ((intptr_t)t.functionPtr & 1) {
return (t.functionPtr - (void**)1);
} else return -1;
}
#elif defined(DEL_USES_MSVC)
template <class Virtual_ptr>
ptrdiff_t _virtual_indexof(Virtual_ptr vfptr) { // for msvc
union {
Virtual_ptr vptr;
uint32_t* uint32_ptr;
uint8_t* uint8_ptr;
} u;
u.vptr = vfptr;
if (*u.uint8_ptr == 0xE9) { // for incremental link
u.uint8_ptr++;
u.uint8_ptr += *u.uint32_ptr + 4;
}
if constexpr (sizeof(nullptr) == 8) { // 64bit
if (*(u.uint32_ptr++) != 0xFF018B48) return -1;
}
else { // 32bit
if ((*(u.uint32_ptr) & 0x00ffffff) != 0xFF018B) return -1;
u.uint8_ptr += 3;
}
switch (*(u.uint8_ptr++)) {
case 0x20: return 0;
case 0x60: return *u.uint8_ptr / sizeof(nullptr);
case 0xA0: return *u.uint32_ptr / sizeof(nullptr);
}
return -1;
}
#endif
// member of Delegate
template <class Obj, class Method>
DEL_INLINE void bind_method(Obj* ptr, Method to_bind) {
clear();
inst = _Simplify_func<sizeof(Method)>::_Convert(ptr, to_bind, fptr);
if constexpr (std::is_polymorphic_v<Obj>) {
auto idx = _virtual_indexof(fptr);
if (idx == -1) return; // is not virtual function
auto vtable = *(_generic_memfunc_ptr_t**)inst;
fptr = vtable[idx];
}
}
_virtual_indexof는 멤버함수를 넘겨주면 런타임에 가상 함수가 아니라면 -1, 가상 함수라면 가상함수 테이블에서 몇 번째에 위치하는지 반환하는 함수입니다. x64/x86 GCC, MSVC에서만 동작하는 것을 확인했고 다른 컴파일러나 아키텍처에서는 동작하지 않을 가능성이 매우 높습니다(모든 컴파일러에서 동작하도록 만들 순 있지만 런타임 비용도 증가하고 코드도 매우 길어집니다). 간단하게 이 함수의 원리를 설명하겠습니다. 먼저 GCC의 경우 멤버 함수 포인터가 가상함수인지, 그렇다면 테이블에서의 인덱스가 무엇인지 그 정보를 담고 있기 때문에 수월하게 가져옵니다. MSVC의 경우 그 정보가 거의 무작위이기 때문에 함수 포인터가 가리키는 곳으로 가서 머신코드를 파싱하는(…) 원리로 가상함수인지, 테이블에서의 인덱스가 무엇인지 가져옵니다. 사실 C++ 표준에만 있으면 컴파일 타임에 정적으로 구현할 수 있는 기능이지만 아쉽게도 없네요.
48번 줄의 bind_method는 멤버함수를 delegate에 바인딩 하는 함수입니다. 38번 줄 부터 살펴보면 먼저 들어온 멤버 함수 포인터의 클래스가 가상함수를 포함하는지 <type_traits>의 std::is_polymorphic을 통해 확인합니다(왜 std::is_virtual은 없는지 모르겠습니다). 만약 그렇다면 들어온 함수가 가상 함수인지 확인한 다음 테이블에서의 인덱스를 구해 실제 함수 포인터를 꺼내옵니다. 이렇게 가상 함수도 효율적으로 바인딩 할 수 있습니다.
다음으로 static 함수를 바인딩 하는 방법을 알아보겠습니다.
// member of _Closure_ptr
template <class Static_func>
DEL_INLINE void bind_static(Static_func to_bind) {
clear();
if (to_bind == nullptr) fptr = nullptr;
else bind_method(this, &_Closure_ptr::static_stub);
// WARNING
inst = reinterpret_cast<_generic_class_t*>(to_bind);
}
// member of _Closure_ptr
// WARNING
Ret static_stub(Args... args) {
using _static_func_ptr_t = Ret(*)(Args...);
return (*reinterpret_cast<_static_func_ptr_t>(this))(std::forward<Args>(args)...);
}
인스턴스의 유무에 따라서 함수를 호출하는 방법이 완전히 달라집니다. 여기선 static_stub이라는 멤버 함수를 대신 바인딩 해서 static 함수를 호출합니다. 여기서 다소 기괴한 방법이 사용됩니다. this 포인터는 위에서 밝혔다시피 그저 첫번째 인자로 넘겨진 포인터 입니다. 따라서 delegate의 인스턴스를 가리키는 포인터는 실제 함수 포인터를 넣고, 멤버 함수 포인터를 넣는 부분에 static_stub의 주소를 넣으면, delegate의 호출 시 static_stub이 실제 호출해야 할 함수 대신 호출됩니다. 여기서 static_stub의 this 포인터가 원래 의미인 delegate의 인스턴스 주소가 아닌 호출해야 할 실제 함수 포인터가 됩니다. 그래서 이 포인터를 호출하면 static 함수를 호출할 수 있습니다. 그냥 delegate의 인스턴스 포인터를 nullptr로 두고 if로 검사해서 호출하면 되지 않냐고 하실 수도 있는데 그럼 멤버 함수를 호출할 때 성능이 같이 떨어질 수 있어서 static 함수 호출을 살짝 희생한 것입니다.
마지막으로 stateful로 분류된 capturing lambda와 functor를 어떻게 바인딩 하는지 살펴보겠습니다.
template<class Ret, class...Args>
struct _Stateful_wrapper abstract {
virtual Ret call(Args... args) = 0;
virtual _generic_class_t* copy() = 0;
virtual size_t size() = 0;
};
// member of _Closure_ptr
template <class Stateful>
DEL_INLINE void bind_stateful(Stateful obj) {
clear();
if constexpr (sizeof(Stateful) > sizeof(nullptr))
impl_bind_dynamic_stateful(obj);
else impl_bind_static_stateful(obj);
}
// member of _Closure_ptr
template <class Stateful>
DEL_INLINE void impl_bind_dynamic_stateful(Stateful obj) {
struct _Wrapper : public _Stateful_wrapper<Ret, Args...> {
_Wrapper(Stateful obj) : obj(obj) {}
Ret call(Args... args) override {
return obj(std::forward<Args>(args)...);
}
_generic_class_t* copy() override {
return reinterpret_cast<_generic_class_t*>(new _Wrapper(*this));
}
size_t size() override {
return sizeof(Stateful);
}
Stateful obj;
};
inst = reinterpret_cast<_generic_class_ptr_t>(new _Wrapper(obj));
fptr = reinterpret_cast<_generic_memfunc_ptr_t>(&_Stateful_wrapper<Ret, Args...>::call);
}
// member of _Closure_ptr
template <class Stateful>
DEL_INLINE void impl_bind_static_stateful(Stateful obj) {
struct _Wrapper {
_Wrapper(Stateful obj) : obj(obj) {}
_Wrapper(const _Wrapper& rhs) : obj(rhs.obj) {}
Ret call(Args... args) {
auto temp = this;
return (*(_Wrapper*)&temp).obj(std::forward<Args>(args)...);
}
Stateful obj;
} wrapper(obj);
inst = *(_generic_class_ptr_t*)&wrapper;
fptr = reinterpret_cast<_generic_memfunc_ptr_t>(&_Wrapper::call);
}
lambda의 경우 바인딩 되었을 때 delegate가 메모리의 소유권을 가져야 합니다. 그렇기 때문에 소멸 시 메모리의 해제, 복사 시 메모리의 복사를 직접 해줘야 합니다. 동적할당을 줄여서 성능을 개선하기 위해 delegate의 인스턴스 포인터인 8바이트에 lambda의 데이터를 집어넣을 수 있다면 동적할당을 하지 않도록 구현했습니다. 먼저 bind_stateful에서 lambda의 크기가 8바이트를 초과하는지 검사합니다. 만약 초과한다면 동적할당을 하는 구현인 impl_bind_dynamic_stateful로 이동합니다.
impl_bind_dynamic_stateful에서 _Stateful_wrapper를 상속받는 _Wrapper를 만들어서 서로 다른 lambda에 대응합니다(bind_stateful이 템플릿 함수이기 때문에 들어오는 lambda마다 서로 다른 _Wrapper가 만들어 집니다). delegate의 인스턴스 포인터에는 동적할당된 _Wrapper의 주소를 넣어주고, 멤버 함수 포인터에는 _Stateful_wrapper::call의 주소를 넣어 줍니다. 24번 줄은 보통 컴파일러에 최적화에 의해 인라인화 됩니다. 따라서 이런 바인딩 방식에선 호출 오버헤드는 가상 함수를 한번 호출하는 것과 같습니다.
동적할당을 하지 않는 구현인 impl_bind_static_stateful은 동적할당을 하는 구현과 비슷합니다. 마찬가지로 51번 줄 또한 보통 컴파일러에 의해 인라인화 되기 때문에 호출 비용은 일반 멤버 함수를 호출하는 것과 거의 같습니다.
// member of _Closure_ptr
DEL_INLINE bool is_dynamic_stateful() const {
return fptr == reinterpret_cast<_generic_memfunc_ptr_t>(&_Stateful_wrapper<Ret, Args...>::call);
}
// member of _Closure_ptr
DEL_INLINE void clear() {
if (is_dynamic_stateful())
delete inst;
inst = nullptr;
fptr = nullptr;
}
// member of _Closure_ptr
DEL_INLINE void copy_from(_Closure_ptr& rhs) {
if (is_dynamic_stateful())
inst = reinterpret_cast<_Stateful_wrapper<Ret, Args...>*>(rhs.inst)->copy();
else inst = rhs.inst;
fptr = rhs.fptr;
}
// member of _Closure_ptr
DEL_INLINE bool is_equal(const _Closure_ptr& rhs) const {
auto stateful0 = is_dynamic_stateful();
auto stateful1 = rhs.is_dynamic_stateful();
if (stateful0 && stateful1)
return (*(_generic_memfunc_ptr_t**)inst)[0] == (*(_generic_memfunc_ptr_t**)rhs.inst)[0];
else if (!stateful0 && !stateful1)
return inst == rhs.inst && fptr == rhs.fptr;
return false;
}
is_dynamic_stateful 함수에서 delegate의 멤버 함수 포인터를 통해 동적할당 된 stateful인지 검사합니다. stateful의 경우 메모리의 소유권이 delegate에 있기 때문에 비교 연산이 애매한 감이 있습니다. 그래서 stateful 객체의 메모리가 완벽히 같아야 진정으로 같은 delegate라고 할 수 있겠지만 여기선 같은 종류의 stateful 객체를 가졌는지 만으로 검사합니다.
가상함수의 특징 때문에 서로 다른 클래스의 가상 함수 포인터가 겹칠 수 있다고 했습니다. _Stateful_wrapper::call()도 가상 함수이기 때문에 외부의 다른 가상 함수와 포인터가 겹칠 수 있습니다(정확히는 외부 클래스의 첫 번째 가상 함수와 주소가 겹침). 하지만 위에서 가상 함수를 바인딩 할 때 가상 함수의 함수 포인터가 아닌 실제 함수의 포인터를 넘겨 줬으므로 이러한 문제는 해결됩니다.
코드
먼저 아래 코드의 별도의 라이센스는 없고 public domain으로 공개하겠습니다. 아래 코드에서 마이크로소프트의 멤버 함수 포인터를 변환하는 부분은 참조 1에서 가져왔음을 밝힙니다. 코드를 사용하시기 전에 컴파일러가 GCC 또는 MSVC(비주얼 스튜디오)인지 확인해 주시기 바랍니다. 다른 컴파일러에서 사용하시려면 _virtual_indexof()를 직접 구현하셔야 합니다. x86이나 x64를 제외한 아키텍처에선 작동하지 않을 가능성이 매우 높습니다.
#pragma once
#include <type_traits>
#include <utility>
#include <cstring> // memcmp
#include <assert.h>
#if defined(__GNUC__) || defined(__GNUG__)
#define DEL_USES_GCC
#define DEL_INLINE inline
#elif defined(_MSC_VER)
#define DEL_USES_MSVC
#define DEL_INLINE __forceinline
#endif
#if defined(DEL_USES_GCC)
template <class Virtual_ptr>
ptrdiff_t _virtual_indexof(Virtual_ptr vfptr) {
typedef struct {
void** functionPtr;
size_t offset;
} MP;
MP& t = *(MP*)&vfptr;
if ((intptr_t)t.functionPtr & 1) {
return (t.functionPtr - (void**)1);
} else return -1;
}
#elif defined(DEL_USES_MSVC)
template <class Virtual_ptr>
ptrdiff_t _virtual_indexof(Virtual_ptr vfptr) {
union {
Virtual_ptr vptr;
uint32_t* uint32_ptr;
uint8_t* uint8_ptr;
} u;
u.vptr = vfptr;
if (*u.uint8_ptr == 0xE9) { // for incremental link
u.uint8_ptr++;
u.uint8_ptr += *u.uint32_ptr + 4;
}
if constexpr (sizeof(nullptr) == 8) { // x64
if (*(u.uint32_ptr++) != 0xFF018B48) return -1;
} else { // x86
if ((*(u.uint32_ptr) & 0x00ffffff) != 0xFF018B) return -1;
u.uint8_ptr += 3;
}
switch (*(u.uint8_ptr++)) {
case 0x20: return 0;
case 0x60: return *u.uint8_ptr / sizeof(nullptr);
case 0xA0: return *u.uint32_ptr / sizeof(nullptr);
}
return -1;
}
#else
template <class Virtual_ptr>
ptrdiff_t _virtual_indexof(Virtual_ptr vfptr) {
static_assert("you have to implement your own");
}
#endif
#ifdef DEL_USES_MSVC
#ifndef __VECTOR_C
class __single_inheritance _generic_class_t;
#endif
class _generic_class_t {};
#else
class _generic_class_t;
#endif
template <int N>
struct _Simplify_func {
template <class Class, class Method, class GenericMemFuncType>
inline static _generic_class_t* _Convert(Class* pthis, Method function_to_bind, GenericMemFuncType& bound_func) {
static_assert("Unsupported member function pointer on_this compiler");
return nullptr;
}
};
constexpr size_t _SINGLE_INHERITANCE_PTR_SIZE = sizeof(void (_generic_class_t::*)());
template <>
struct _Simplify_func<_SINGLE_INHERITANCE_PTR_SIZE> {
template <class Class, class Method, class GenericMemFuncType>
inline static _generic_class_t* _Convert(Class* pthis, Method function_to_bind, GenericMemFuncType& bound_func) {
bound_func = reinterpret_cast<GenericMemFuncType>(function_to_bind);
return reinterpret_cast<_generic_class_t*>(pthis);
}
};
#ifdef DEL_USES_MSVC
struct _generic_virtual_class_t : virtual public _generic_class_t {
_generic_virtual_class_t* _This() { return this; }
using _ProbePtrType = decltype(&_This);
};
constexpr size_t _MULTIPLE_INHERITANCE_PTR_SIZE = _SINGLE_INHERITANCE_PTR_SIZE + sizeof(int) * 1;
constexpr size_t _VIRTUAL_INHERITANCE_PTR_SIZE = _SINGLE_INHERITANCE_PTR_SIZE + sizeof(int) * 2;
constexpr size_t _UNKNOWN_INHERITANCE_PTR_SIZE = _SINGLE_INHERITANCE_PTR_SIZE + sizeof(int) * 3;
template<>
struct _Simplify_func<_MULTIPLE_INHERITANCE_PTR_SIZE> {
template <class Class, class Method, class GenericMemFuncType>
inline static _generic_class_t* _Convert(Class* pthis, Method function_to_bind, GenericMemFuncType& bound_func) {
// In MSVC, a multiple inheritance member pointer is internally defined as:
union {
Method func;
struct {
GenericMemFuncType funcaddress;
int delta;
}s;
} u;
static_assert(sizeof(function_to_bind) == sizeof(u.s));
u.func = function_to_bind;
bound_func = u.s.funcaddress;
return reinterpret_cast<_generic_class_t*>(reinterpret_cast<char*>(pthis) + u.s.delta);
}
};
template <>
struct _Simplify_func<_VIRTUAL_INHERITANCE_PTR_SIZE> {
template <class Class, class Method, class GenericMemFuncType>
inline static _generic_class_t* _Convert(Class* pthis, Method function_to_bind, GenericMemFuncType& bound_func) {
struct MicrosoftVirtualMFP {
void (_generic_class_t::* codeptr)(); // points to the actual member function
int delta; // #bytes to be added to the 'this' pointer
int vtable_index; // or 0 if no virtual inheritance
};
union {
Method func;
_generic_class_t* (Class::* ProbeFunc)();
MicrosoftVirtualMFP s;
} u;
union {
_generic_virtual_class_t::_ProbePtrType virtfunc;
MicrosoftVirtualMFP s;
} u2;
static_assert(sizeof(function_to_bind) == sizeof(u.s));
static_assert(sizeof(function_to_bind) == sizeof(u.ProbeFunc));
static_assert(sizeof(u2.virtfunc) == sizeof(u2.s));
u.func = function_to_bind;
bound_func = reinterpret_cast<GenericMemFuncType>(u.s.codeptr);
u2.virtfunc = &_generic_virtual_class_t::_This;
u.s.codeptr = u2.s.codeptr;
return (pthis->*u.ProbeFunc)();
}
};
template <>
struct _Simplify_func<_UNKNOWN_INHERITANCE_PTR_SIZE> {
template <class Class, class Method, class GenericMemFuncType>
inline static _generic_class_t* _Convert(Class* pthis, Method function_to_bind, GenericMemFuncType& bound_func) {
union {
Method func;
struct {
GenericMemFuncType m_funcaddress;
int delta;
int vtordisp;
int vtable_index;
} s;
} u;
static_assert(sizeof(Method) == sizeof(u.s));
u.func = function_to_bind;
bound_func = u.s.funcaddress;
int virtual_delta = 0;
if (u.s.vtable_index) {
const int* vtable = *reinterpret_cast<const int* const*>(
reinterpret_cast<const char*>(pthis) + u.s.vtordisp);
virtual_delta = u.s.vtordisp + *reinterpret_cast<const int*>(
reinterpret_cast<const char*>(vtable) + u.s.vtable_index);
}
return reinterpret_cast<_generic_class_t*>(reinterpret_cast<char*>(pthis) + u.s.delta + virtual_delta);
};
};
#endif // MS/Intel hacks
template<class Ret, class...Args>
struct _Stateful_wrapper abstract {
virtual Ret call(Args... args) = 0;
virtual _generic_class_t* copy() = 0;
};
template<class T>
struct _Closure_ptr;
template<class Ret, class...Args>
struct _Closure_ptr<Ret(Args...)> {
using _generic_class_ptr_t = _generic_class_t*;
using _generic_memfunc_ptr_t = void (_generic_class_t::*)();
using _fptr_t = Ret(_generic_class_t::*)(Args...);
DEL_INLINE _generic_class_ptr_t get_inst() const {
return inst;
}
DEL_INLINE _fptr_t get_fptr() const {
return reinterpret_cast<_fptr_t>(fptr);
}
template <class Obj, class Method>
DEL_INLINE void bind_method(Obj* ptr, Method to_bind) {
clear();
inst = _Simplify_func<sizeof(Method)>::_Convert(ptr, to_bind, fptr);
if constexpr (std::is_polymorphic_v<Obj>) {
auto idx = _virtual_indexof(fptr);
if (idx == -1) return;
auto vtable = *(_generic_memfunc_ptr_t**)inst;
fptr = vtable[idx];
}
}
template <class Obj, class Method>
DEL_INLINE void bind_method(const Obj* ptr, Method to_bind) {
bind_method(const_cast<Obj*>(ptr), to_bind);
}
template <class Static_func>
DEL_INLINE void bind_static(Static_func to_bind) {
clear();
if (to_bind == nullptr) fptr = nullptr;
else bind_method(this, &_Closure_ptr::static_stub);
// WARNING
inst = reinterpret_cast<_generic_class_t*>(to_bind);
}
template <class Stateful>
DEL_INLINE void bind_stateful(Stateful obj) {
clear();
if constexpr (sizeof(Stateful) > sizeof(nullptr))
impl_bind_dynamic_stateful(std::forward<Stateful>(obj));
else impl_bind_static_stateful(std::forward<Stateful>(obj));
}
DEL_INLINE void copy_from(const _Closure_ptr& rhs) {
if (is_dynamic_stateful())
inst = reinterpret_cast<_Stateful_wrapper<Ret, Args...>*>(rhs.inst)->copy();
else inst = rhs.inst;
fptr = rhs.fptr;
}
DEL_INLINE void move_from(_Closure_ptr&& rhs) {
inst = std::exchange(rhs.inst, nullptr);
fptr = std::exchange(rhs.fptr, nullptr);
}
DEL_INLINE void clear() {
if (is_dynamic_stateful())
delete inst;
inst = nullptr;
fptr = nullptr;
}
DEL_INLINE bool empty() const {
return !(inst || fptr);
}
DEL_INLINE bool is_static() const {
return fptr == reinterpret_cast<_generic_memfunc_ptr_t>(&_Closure_ptr::static_stub);
}
DEL_INLINE bool is_dynamic_stateful() const {
return fptr == reinterpret_cast<_generic_memfunc_ptr_t>(&_Stateful_wrapper<Ret, Args...>::call);
}
DEL_INLINE bool is_equal_static(Ret(*rhs)(Args...)) const {
return inst == rhs;
}
DEL_INLINE bool is_equal(const _Closure_ptr& rhs) const {
auto stateful0 = is_dynamic_stateful();
auto stateful1 = rhs.is_dynamic_stateful();
if (stateful0 && stateful1)
return (*(_generic_memfunc_ptr_t**)inst)[0] == (*(_generic_memfunc_ptr_t**)rhs.inst)[0];
else if (!stateful0 && !stateful1)
return inst == rhs.inst && fptr == rhs.fptr;
return false;
}
private:
template <class Stateful>
DEL_INLINE void impl_bind_dynamic_stateful(Stateful&& obj) {
struct _Wrapper : public _Stateful_wrapper<Ret, Args...> {
_Wrapper(Stateful&& obj) : obj(std::forward<Stateful>(obj)) {}
Ret call(Args... args) override {
return obj.operator()(std::forward<Args>(args)...);
}
_generic_class_t* copy() override {
return reinterpret_cast<_generic_class_t*>(new _Wrapper(*this));
}
Stateful obj;
};
inst = reinterpret_cast<_generic_class_ptr_t>(new _Wrapper(std::forward<Stateful>(obj)));
fptr = reinterpret_cast<_generic_memfunc_ptr_t>(&_Stateful_wrapper<Ret, Args...>::call);
}
template <class Stateful>
DEL_INLINE void impl_bind_static_stateful(Stateful&& obj) {
struct _Wrapper {
_Wrapper(Stateful&& obj) : obj(std::forward<Stateful>(obj)) {}
Ret call(Args... args) {
auto temp = this;
return (*(_Wrapper*)&temp).obj.operator()(std::forward<Args>(args)...);
}
Stateful obj;
} wrapper(std::forward<Stateful>(obj));
inst = *(_generic_class_ptr_t*)&wrapper;
fptr = reinterpret_cast<_generic_memfunc_ptr_t>(&_Wrapper::call);
}
// WARNING
Ret static_stub(Args... args) {
using _static_func_ptr_t = Ret(*)(Args...);
return (*reinterpret_cast<_static_func_ptr_t>(this))(std::forward<Args>(args)...);
}
_generic_class_ptr_t inst = nullptr;
_generic_memfunc_ptr_t fptr = nullptr;
};
template <typename T> class Delegate;
template<class Ret, class...Args>
class Delegate<Ret(Args...)> {
template<class Ret, class...Args>
struct _Stateful_wrapper abstract {
virtual Ret call(Args... args) = 0;
virtual _generic_class_t* copy() = 0;
};
public:
using type = Delegate;
using return_t = Ret;
DEL_INLINE Delegate() {
closure.clear();
}
DEL_INLINE Delegate(Ret(*function_to_bind)(Args...)) {
closure.bind_static(function_to_bind);
}
template <class Class, class Obj>
DEL_INLINE Delegate(Obj* obj, Ret(Class::* to_bind)(Args...)) {
closure.bind_method(static_cast<Class*>(obj), to_bind);
}
template <class Class, class Obj>
DEL_INLINE Delegate(const Obj* obj, Ret(Class::* to_bind)(Args...) const) {
closure.bind_method(static_cast<const Class*>(obj), to_bind);
}
template <class Lambda, class _Ret, class...Args, std::enable_if_t<!std::is_same_v<Lambda, Delegate<_Ret(Args...)>>, int> = 0>
DEL_INLINE Delegate(Lambda&& lambda) {
bind(std::forward<Lambda>(lambda));
}
DEL_INLINE Delegate(const Delegate& rhs) {
closure.copy_from(rhs.closure);
}
DEL_INLINE Delegate(Delegate&& rhs) noexcept {
closure.move_from(std::move(rhs.closure));
}
DEL_INLINE ~Delegate() {
closure.clear();
}
DEL_INLINE Delegate& operator=(const Delegate& rhs) {
closure.copy_from(rhs.closure);
return *this;
}
DEL_INLINE Delegate& operator=(Delegate&& rhs) noexcept {
closure.move_from(rhs.closure);
return *this;
}
DEL_INLINE Delegate& operator=(Ret(*function_to_bind)(Args...)) {
closure.bind_static(function_to_bind);
return *this;
}
template <class Lambda, class _Ret, class...Args, std::enable_if_t<!std::is_same_v<Lambda, Delegate<_Ret(Args...)>>, int> = 0>
DEL_INLINE Delegate& operator=(Lambda&& lambda) {
bind(std::forward<Lambda>(lambda));
return *this;
}
DEL_INLINE void bind(Ret(*function_to_bind)(Args...)) {
closure.bind_static(this, &Delegate::invokeStatic, function_to_bind);
}
template <class Class, class Obj>
DEL_INLINE void bind(Obj* pthis, Ret(Class::* function_to_bind)(Args...)) {
closure.bind_method(static_cast<Class*>(pthis), function_to_bind);
}
template <class Class, class Obj>
DEL_INLINE void bind(const Obj* pthis, Ret(Class::* function_to_bind)(Args...) const) {
closure.bind_method(static_cast<const Class*>(pthis), function_to_bind);
}
template <class Lambda, class _Ret, class...Args, std::enable_if_t<!std::is_same_v<Lambda, Delegate<_Ret(Args...)>>, int> = 0>
DEL_INLINE void bind(Lambda&& lambda) {
if constexpr (std::is_convertible_v<Lambda, Ret(*)(Args...)>)
closure.bind_static((Ret(*)(Args...))lambda);
else closure.bind_stateful(std::forward<Lambda>(lambda));
}
DEL_INLINE void clear() {
closure.clear();
}
DEL_INLINE Ret operator()(Args... args) const {
return (closure.get_inst()->*(closure.get_fptr()))(std::forward<Args>(args)...);
}
DEL_INLINE Ret invoke(Args... args) const {
assert(!closure.empty() && "tried invoking empty delegate");
return (closure.get_inst()->*(closure.get_fptr()))(std::forward<Args>(args)...);
}
DEL_INLINE bool empty() const {
return closure.empty();
}
DEL_INLINE operator bool() const {
return !empty();
}
DEL_INLINE void* instance() const {
return closure.is_static() ? nullptr : closure.get_inst();
}
DEL_INLINE bool operator==(Ret(*funcptr)(Args...)) const {
return closure.is_equal_static(funcptr);
}
DEL_INLINE bool operator!=(Ret(*funcptr)(Args...)) const {
return !closure.is_equal_static(funcptr);
}
DEL_INLINE bool operator==(const Delegate& rhs) const {
return closure.is_equal(rhs.closure);
}
DEL_INLINE bool operator!=(const Delegate& rhs) const {
return !closure.is_equal(rhs.closure);
}
private:
_Closure_ptr<Ret(Args...)> closure;
};
// Helper functions
template <class Ret, class... Args>
DEL_INLINE Delegate<Ret(Args...)> make_delegate(Ret(*func)(Args...)) {
return { func };
}
template <class Class, class Obj, class Ret, class... Args>
DEL_INLINE Delegate<Ret(Args...)> make_delegate(Obj* instance, Ret(Class::* func)(Args...)) {
return { instance, func };
}
template <class Class, class Obj, class Ret, class... Args>
DEL_INLINE Delegate<Ret(Args...)> make_delegate(Obj* instance, Ret(Class::* func)(Args...) const) {
return { instance, func };
}
C++데모
#include <iostream>
#include "delegate.hpp"
using namespace std;
void static_function(int a) {
cout << "invoking static_function, a = " << a << endl;
}
class A {
public:
void member_function(int a) const {
cout << "invoking member_function, a = " << a << endl;
}
void virtual_function(int a) {
cout << "invoking virtual_function, a = " << a << endl;
}
};
int main() {
A inst1, inst2;
int b = 78978;
Delegate<void(int)> del0 = static_function;
auto del1 = make_delegate(&inst1, &A::member_function);
auto del1_dup = make_delegate(&inst1, &A::member_function);
Delegate<void(int)> del2;
del2.bind(&inst2, &A::member_function);
auto del3 = make_delegate(&inst1, &A::virtual_function);
Delegate<void(int)> del4 = [](int a) { cout << "invoking non_capturing_lambda, a = " << a << endl; };
Delegate<void(int)> del5 = [&b](int a) { cout << "invoking capturing_lambda, a = " << a << ", b = " << b << endl; };
del0.invoke(0);
del1.invoke(1);
del2.invoke(2);
del3.invoke(3);
del4(4);
del5(5);
if (del1 == del1_dup) cout << "del1 == del1_dup\n";
else cout << "del1 == del1_dup\n";
if (del1 == del2) cout << "del1 == del2\n";
else cout << "del1 != del2\n";
return 0;
}
C++delegate를 생성하기 위해서 생성자를 사용하거나, make_delegate 편의 함수를 사용하거나, 비어있거나 바인딩 된 delegate의 bind()를 호출할 수 있습니다. 호출하기 위해선 invoke()나 오버로딩된 operator()를 사용 할 수 있습니다. 차이점은 invoke()의 경우 디버그 빌드에서 빈 delegate를 호출하려 했을 때 에러가 발생합니다.
더 생각해보기
- Calling Convention
- CV Qualifier
- lambda나 functor의 비교연산
- is_virtual<T> 구현
- 컴파일러에 의존적이지 않은 _virtual_indexof 구현
- x64에서 포인터 자료형 압축
참조
[1] https://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible
[4]https://www.codeproject.com/Articles/18389/Fast-C-Delegate-Boost-Function-drop-in-replacement
최신 댓글