Avoid memory allocation with std::function and member function

前端 未结 5 1384
野的像风
野的像风 2021-01-31 16:24

This code is just for illustrating the question.

#include 
struct MyCallBack {
    void Fire() {
    }
};

int main()
{
    MyCallBack cb;
             


        
5条回答
  •  攒了一身酷
    2021-01-31 17:03

    I propose a custom class for your specific usage.

    While it's true that you shouldn't try to re-implement existing library functionality because the library ones will be much more tested and optimized, it's also true that it applies for the general case. If you have a particular situation like in your example and the standard implementation doesn't suite your needs you can explore implementing a version tailored to your specific use case, which you can measure and tweak as necessary.

    So I have created a class akin to std::function that works only for methods and has all the storage in place (no dynamic allocations).

    I have lovingly called it Trigger (inspired by your Fire method name). Please do give it a more suited name if you want to.

    // helper alias for method
    // can be used in user code
    template 
    using Trigger_method = auto (T::*)() -> void;
    
    namespace detail
    {
    
    // Polymorphic classes needed for type erasure
    struct Trigger_base
    {
        virtual ~Trigger_base() noexcept = default;
        virtual auto placement_clone(void* buffer) const noexcept -> Trigger_base* = 0;
    
        virtual auto call() -> void = 0;
    };
    
    template 
    struct Trigger_actual : Trigger_base
    {
        T& obj;
        Trigger_method method;
    
        Trigger_actual(T& obj, Trigger_method method) noexcept : obj{obj}, method{method}
        {
        }
    
        auto placement_clone(void* buffer) const noexcept -> Trigger_base* override
        {
            return new (buffer) Trigger_actual{obj, method};
        }
    
        auto call() -> void override
        {
            return (obj.*method)();
        }
    };
    
    // in Trigger (bellow) we need to allocate enough storage
    // for any Trigger_actual template instantiation
    // since all templates basically contain 2 pointers
    // we assume (and test it with static_asserts)
    // that all will have the same size
    // we will use Trigger_actual
    // to determine the size of all Trigger_actual templates
    struct Trigger_test_size {};
    
    }
    
    struct Trigger
    {
        std::aligned_storage_t)>
            trigger_actual_storage_;
    
        // vital. We cannot just cast `&trigger_actual_storage_` to `Trigger_base*`
        // because there is no guarantee by the standard that
        // the base pointer will point to the start of the derived object
        // so we need to store separately  the base pointer
        detail::Trigger_base* base_ptr = nullptr;
    
        template 
        Trigger(X& x, Trigger_method method) noexcept
        {
            static_assert(sizeof(trigger_actual_storage_) >= 
                             sizeof(detail::Trigger_actual));
            static_assert(alignof(decltype(trigger_actual_storage_)) %
                             alignof(detail::Trigger_actual) == 0);
    
            base_ptr = new (&trigger_actual_storage_) detail::Trigger_actual{x, method};
        }
    
        Trigger(const Trigger& other) noexcept
        {
            if (other.base_ptr)
            {
                base_ptr = other.base_ptr->placement_clone(&trigger_actual_storage_);
            }
        }
    
        auto operator=(const Trigger& other) noexcept -> Trigger&
        {
            destroy_actual();
    
            if (other.base_ptr)
            {
                base_ptr = other.base_ptr->placement_clone(&trigger_actual_storage_);
            }
    
            return *this;
        }
    
        ~Trigger() noexcept
        {
            destroy_actual();
        }
    
        auto destroy_actual() noexcept -> void
        {
            if (base_ptr)
            {
                base_ptr->~Trigger_base();
                base_ptr = nullptr;
            }
        }
    
        auto operator()() const
        {
            if (!base_ptr)
            {
                // deal with this situation (error or just ignore and return)
            }
    
            base_ptr->call();
        }
    };
    

    Usage:

    struct X
    {    
        auto foo() -> void;
    };
    
    
    auto test()
    {
        X x;
    
        Trigger f{x, &X::foo};
    
        f();
    }
    

    Warning: only tested for compilation errors.

    You need to thoroughly test it for correctness.

    You need to profile it and see if it has a better performance than other solutions. The advantage of this is because it's in house cooked you can make tweaks to the implementation to increase performance on your specific scenarios.

提交回复
热议问题