Search code examples
c++c++14variadic-templatesperfect-forwardingstdbind

Perfect forwarding of references with std::bind inside variadic template


I stumbled upon a bug in my code which I traced down to the fact that arguments to std::bind "...are never passed by reference unless wrapped in std::ref or std::cref".

What I have

I have a function template which looks like this (stripped of irrelevant bits):

template<typename F, typename ... Args>
auto eval(F&& _f, Args&&... _args)
{
    using ret_t = typename std::result_of<F&&(Args&&...)>::type;

    std::function<ret_t()> func(std::bind(std::forward<F>(_f), std::forward<Args>(_args)...)));
    // Etc.
}

So far so good, but this breaks down if function _f takes references as arguments because the arguments are copied or moved unless wrapped in std::ref or std::cref.

What I would like

A way to pass arguments as references to _f while preserving perfect forwarding as much as possible. I would very much like to avoid having to pass each of the _args by wrapping it in std::ref at the origin. Rather, I would like the references to be figured out automagically inside eval().

What I have tried

Using std::ref instead of std::forward when binding _args seems to work:

template<typename F, typename ... Args>
auto eval(F&& _f, Args&&... _args)
{
    using ret_t = typename std::result_of<F&&(Args&&...)>::type;

    // Replace std::forward<Args>() with std::ref()
    std::function<ret_t()> func(std::bind(std::forward<F>(_f), std::ref(_args)...))); 
    // Etc.
}

But I don't know how this behaves with rvalue references. Is it safe to always use std::ref? Is it an overkill? Do I still have perfect forwarding? Questions abound.

I have also considered replacing std::bind with a generic lambda, but I am not sure how to capture the arguments in order to achieve perfect forwarding.

Any insight would be appreciated.


Solution

  • Is it safe to always use std::ref?

    It's as safe as using references normally. So in this case, if func does not outlive the function eval(), then it's safe. Even if I pass in rvalues, the references won't dangle. But if you need to store func somewhere, then this is a recipe for dangling references.

    What you would want to do is conditionally wrap them as references. One way would be to provide two overloads for lvalues and rvalues:

    template <class T> std::reference_wrapper<T> maybe_wrap(T& val) { return std::ref(val); }
    template <class T> T&& maybe_wrap(T&& val) { return std::forward<T>(val); }
    

    lvalues turn into reference wrappers, rvalues stay as rvalue references. Now:

    std::function<ret_t()> func(std::bind(std::forward<F>(_f), 
        maybe_wrap(std::forward<Args>(_args))...)); 
    

    is safe.


    Note, the way the bind expression will be invoked and the way you're determining the return type don't quite match. All the bound arguments are passed as lvalues, so what you really need is:

    using ret_t = typename std::result_of<F&(Args&...)>::type;
    //                                    ^^^^^^^^^^^
    

    You can also replace this with a lambda, which is awkward because of the limited ways to capture parameter packs. You'd have to go through a tuple and implement std::apply() (which is doable in C++14):

    [f=std::forward<F>(f),
        args=std::make_tuple(std::forward<Args>(_args)...)]() -> decltype(auto)
    {
        return std::apply(f, args);
    }
    

    Though, due to the necessary hurdles and the fact that this is going to be type erased anyway, maybe bind() is better.