Search code examples
c++shared-ptrunique-ptrstd-function

Should you use std::unique_ptr alongside std::function?


I'm having trouble figuring out the best way to deal with allocation and deallocation of std::function.

Of course the underlying function is never going to be deallocated, but the captured variables etc. need to be deallocated.

I presume that this is deallocated inside the destructor of std::function.

Does this mean the best way to manage functions that persist is through the use of std::unique_ptr<std::function<...>> and std::shared_ptr<std::function<...>>, or even just a raw pointer std::function<...>*?

Wouldn't this be an extra pointer of indirection? Is this the recommended way to go?


Solution

  • Remember that std::function is not a lambda. A std::function don't have captures. It's a polymorphic wrapper around a function like object like a lambda.

    A (extremely rough and incomlete and incorrect) representation of std::function looks like this:

    template<typename R, typename... Args>
    struct function {
        function(auto lambda) :
            fptr{[](void* l, Args...){ static_cast<decltype(lambda)>(l)(std::forward<Args>(args)...); }}
            function_object{new auto(lambda)} {}
        
        auto operator()(Args...) const -> R {
            return fptr(function_object, std::forward<Args>(args)...);
        }
    
        // other members...
    
    private:
        R(*fptr)(void*, Args...);
        void* function_object;
    };
    

    As you can see, the function_object member is dynamically allocated. This is the lambda, and the lambda contains the captures. They will only be deleted when the std::function destructor is called.

    Then, also remember that std::function respects value semantics. This means that if the std::function is copied, the lambda held inside is also copied. This means you can freely copy, move around, make some copies out of scope, call the function and it won't affect other copies.

    This means this code works:

    auto make_function_with_captures(double a, double b) -> std::function<double()> {
        // captures 'a' and 'b' as values
        auto lambda = [a, b] {
            return a + b;
        };
    
        // Here, 'lambda' is copied in the std::function
        return std::function<double()>{lambda};
        
    } // 'lambda' goes out of scope here, with its captures
    
    int main() {
        auto function = make_function_with_captures(1.2, 4.5);
    
        // Here, use a copy of the lambda that was created in the function        
        double a = function();
    }
    

    Wouldn't this be an extra pointer of indirection?

    Yes. This would be two pointer indirection with a virtual call.

    Is this the recommended way to go?

    I would answer this question to "no" most of the time, like 98% of cases.