7
\$\begingroup\$

I've been trying to implement delegate for my event system. I thought that maybe std::function can do the trick for me, but it turned out to be not comparable, so what i have end up doing is this - std::function replacement.

It allows to bind function pointers to TFunction object and call it later with specified parameters like the std::function does.

It returns what unredlying function returns.

TFunction class is trivially constructable, so move/copy constructors and operators are defined implicitly.

I use it in a context of event & delegate to subscribe callbacks on application events.

TFunction.h:

#pragma once

using uint64 = unsigned long long;

#if _WIN64
#pragma pointers_to_members( full_generality, multiple_inheritance )
#endif

namespace t3d
{
template<typename T> class TFunction;

template<typename Return_T, typename... Args_T>
class TFunction<Return_T(Args_T...)>
{
public:
    
// Aliases:

    template<class C>
    using CallbackMember_T = Return_T(C::*)(Args_T...);
    
    using CallbackStatic_T = Return_T(*)(Args_T...);

// Constructors:

    TFunction()
        : CallbackMember   (nullptr)
        , FunctionPointers { &TFunction::CallMember, &TFunction::CallStatic } 
        , CallbackStatic   (nullptr)
        , Instance         (nullptr)
        , Index            (0)
    {
    }

    template<class C>
    TFunction(C* Instance, CallbackMember_T<C> Callback)
        : CallbackMember   (reinterpret_cast<CallbackMember_T<FInstance>>(Callback))
        , FunctionPointers { &TFunction::CallMember, &TFunction::CallStatic }
        , CallbackStatic   (nullptr)
        , Instance         (reinterpret_cast<FInstance*>(Instance))
        , Index            (0)
    {
    }

    TFunction(CallbackStatic_T Callback)
        : CallbackMember   (nullptr)
        , FunctionPointers { &TFunction::CallMember ,  &TFunction::CallStatic }
        , CallbackStatic   (Callback)
        , Instance         (nullptr)
        , Index            (1)
    {
    }

// Functions:

    template<class C>
    constexpr void Bind(C* Instance, CallbackMember_T<C> Callback) noexcept
    {
        this->Instance       = reinterpret_cast<FInstance*>(Instance);
        this->CallbackMember = reinterpret_cast<CallbackMember_T<FInstance>>(Callback);
        this->CallbackStatic = nullptr;
        this->Index          = 0;
    }

    constexpr void Bind(CallbackStatic_T Callback) noexcept
    {
        this->Instance       = nullptr;
        this->CallbackMember = nullptr;
        this->CallbackStatic = Callback;
        this->Index          = 1;
    }

    constexpr Return_T Invoke(Args_T... Args) const
    {
        return (this->*FunctionPointers[Index])(std::forward<Args_T>(Args)...);
    }

    template<class C>
    constexpr bool8 IsEqual(CallbackMember_T<C> Callback) const
    {
        return reinterpret_cast<CallbackMember_T<C>>(CallbackMember) == Callback;
    }

    constexpr bool8 IsEqual(CallbackStatic_T Callback) const noexcept
    {
        return CallbackStatic == Callback;
    }

    constexpr bool8 IsEmpty() const noexcept
    {
        return (Instance == nullptr) && (CallbackMember == nullptr) && (CallbackStatic == nullptr);
    }

// Operators:

    explicit operator bool () const noexcept
    {
        return (Instance && CallbackMember) || CallbackStatic;
    }

    constexpr bool8 operator == (const TFunction& Right) const noexcept
    {
        return (Instance == Right.Instance) && (CallbackMember == Right.CallbackMember) && (CallbackStatic == Right.CallbackStatic);
    }

    constexpr bool8 operator != (const TFunction& Right) const noexcept
    {
        return (Instance != Right.Instance) || (CallbackMember != Right.CallbackMember) || (CallbackStatic != Right.CallbackStatic);
    }

private:

    using CallbackInternal_T = Return_T(TFunction::*)(Args_T...) const;

// Private Functions:

    Return_T CallMember(Args_T... Args) const
    {
        return (Instance->*CallbackMember)(std::forward<Args_T>(Args)...);
    }

    Return_T CallStatic(Args_T... Args) const
    {
        return (*CallbackStatic)(std::forward<Args_T>(Args)...);
    }

// Variables:

    class FInstance {};

    CallbackMember_T<FInstance> CallbackMember;
    CallbackInternal_T          FunctionPointers[2];        
    CallbackStatic_T            CallbackStatic;
    FInstance*                  Instance;
    uint64                      Index;
};
}

Usage example:

Main.cpp:

struct FData
{
    int A;
    int B;
};

struct FResult
{
    bool Success;
};

class FClass
{
public:

    FResult Function(FData Data)
    {
        return FResult{ true };
    }
};

int main()
{
    FClass Class;

// Example 1 (Default construction):

    t3d::TFunction<FResult(FData)> TestFunction;
    
    TestFunction.Bind(&Class, &FClass::Function);
    
    TestFunction.Invoke(FData{ 1, 2 });

// Example 2 (Parametrized construction):

    t3d::TFunction<FResult(FData)> TestFunction(&Class, &FClass::Function);

    TestFunction.Invoke(FData{ 1, 2 });

    return 0;
}

Supported target types: Member virtual/non-virtual function pointer, Static/global function pointer;

Can be compared for equality to TFunction or any function pointer/address.

It 3 times slower than function call, does not allocate and is 7 times faster than std::function, but at expense of not supporting lambda expressions and functors.

This is by design however, it supposed to be used as a delegate for event system like this:

using Delegate_T = TFunction<EPropogation(T)>

Any suggestions?

\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

Design

  1. Tell us what your code does, and also add such a comment to the code.

    As far as I can tell, it adds equality-comparability, by dropping type-erasure, thus dropping support for all but two specific target types.
    That's a severe loss of functionality and should be called out.

  2. Anyway, your design seems to march to its own drum. Consider adhering to the appropriate standard interface to ease adoption and limit surprises.

    That means fixing the names, and making it actually callable!

Target-Architecture

  1. Is there a specific reason you cater to Microsoft member function pointer peculiarities in Windows 64 Bit, but not 32 Bit?
    Just use _WIN32 instead.

General critic

  1. Don't define your own uint64. Just use uint64_t from <cstdint>. That is, if you actually need such a precisely defined type.

  2. FInstance should be an opaque type, meaning it is only ever declared, never defined.

  3. You could simplify .Bind() by reusing ctor and assignment.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for advice, i edited post to specify supported target types, and also removed definition for FInstance since it is actually redundant. The reason for using _WIN64 is that Iam not targeting 32-bit architecture. This is not by any means pretending to be std::function replacement in the standard. I will consider to make TFunction Callable. Tell me please more what do you mean by saying: "Use noexcept and constexpr where appropriate." \$\endgroup\$
    – Lex
    Jul 4, 2022 at 15:33
  • \$\begingroup\$ @Lex Removed last point. And even if you don't pretend to be an (enhanced) replacement for std::function, keeping the interface conventional is best. I don't see anything 64bit specific, aside from the pragma being excluded there, thus it looks like a gratuitous limitation. Hm, forgot to vote... \$\endgroup\$ Jul 4, 2022 at 16:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.