9
\$\begingroup\$

One of the things that has been really exciting me in c++20 is std::bind_front. Using placeholders with std::bind and boost::bind has really bothered me and the code looked messier and messier with each call to bind. It was bad enough for me to decide to enable -std=c++2a and pray that I wouldn't have to fix the code in the future if something gets changed.

I was messing around with parameter pack recursion when I realized I could make a std::bind_front alternative that would work in c++17. If I removed the std::invoke it would even work in c++11.

It even seems to compile faster than my standard library's implementation.


//#include <utility>
//#include <functional>

//of course this would go into some kind of namespace

template <class F, class A>
struct _bind_obj {
    F originalFunc;
    A arg;
    template <class... Args>
    inline auto operator()(Args&&... a){
        return std::invoke(originalFunc, arg, std::forward<Args>(a)...);
    }
    _bind_obj(F &&_originalFunc, A &&_arg) :
        originalFunc(std::forward<F>(_originalFunc)),
        arg(std::forward<A>(_arg)){

    }
};

template <class F, class A>
auto bind_front(F &&func, A &&arg){
    return _bind_obj<F, A>(
        std::forward<F>(func),
        std::forward<A>(arg)
    );
}

template <class F, class FirstA, class... A>
auto bind_front(F &&func, FirstA &&firstA, A&&... a){
    return bind_front(
        bind_front(
            std::forward<F>(func),
            std::forward<FirstA>(firstA)
        ),
        std::forward<A>(a)...
    );
}

So what do you think? Are there some cases in std::bind_front that I missed? Are there any optimizations I should make?

\$\endgroup\$
5
\$\begingroup\$

bind_front can (and should) be made constexpr.

The callable object and the bound arguments need to be decayed per the standard.

You can store all arguments in a tuple instead of generating nested wrappers:

template <class FD, class... Args>
class bind_obj {
    // ...
    FD func;
    std::tuple<Args...> args;
};

and then call

std::apply(func, args, std::forward<A>(call_args)...)

(which internally calls invoke.)

Otherwise, nice code.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks! I see an improvement in compile time and executable size with constexpr. Out of curiosity, what are the advantages of using std::apply and tuples over nested wrappers? \$\endgroup\$
    – user233009
    Sep 15 '19 at 18:56
  • 1
    \$\begingroup\$ @user233009 You are welcome. With nested wrappers, the compiler has to instantiate the template multiple times (n instantiations of bind_obj for n args) and there are multiple function calls (think of f1(f2(f3(f4(f5, a5), a4), a3), a2), a1) ... scary) whereas std::apply simply expands the template parameter pack, resulting a single function call (f(a1, a2, a3, a4, a5)). That should explain the reduction in both compile time and executable size. \$\endgroup\$
    – L. F.
    Sep 16 '19 at 9:59
8
\$\begingroup\$

Are there some cases in std::bind_front that I missed?

There are several significant differences between std::bind_front's behavior and your implementations.


First, your implementation unconditionally returns a value. But what if the callable in question returned a reference? The behavior is just incorrect.


Second, std::bind_front is SFINAE-friendly but yours is not. That is, I can check to see if I can invoke the result with a certain set of arguments. Here is a silly example demonstrating this:

auto f = [](int i, int j) { return i + j; };
auto g = std::bind_front(f, 1);
auto g2 = your::bind_front(f, 1);

// this is ok, static assertion doesn't trigger
static_assert(!std::is_invocable_v<decltype(g), int, int>);

// this is a compile error
static_assert(!std::is_invocable_v<decltype(g2), int, int>);

Basically, asking the question "Can I call you with two ints?" leads to an instantiation failure outside of the immediate context, and will always be a hard error.

You can fix this and the reference issue described above by specified the correct return type instead of auto:

template <class... Args>
inline std::invoke_result_t<F&, A, Args...> operator()(Args&&... a) { ... }

Third, you're capturing your arguments differently. Consider:

std::string s = "Hello";
auto f = [](std::string const& s, int i) { return s[i]; }

auto g = std::bind_front(f, s);
auto g2 = your::bind_front(f, s);
s = "Goodbye";

assert(g(0) == 'H'); // ok
assert(g2(0) == 'H'); // fails

std::bind_front owns everything. You keep references to all the lvalues. The way the standard library works is if you want to capture by reference, you use std::ref.

Basically, this:

template <class F, class A>
auto bind_front(F &&func, A &&arg){
    return _bind_obj<F, A>(
        std::forward<F>(func),
        std::forward<A>(arg)
    );
}

should be:

template <class F, class A>
auto bind_front(F &&func, A &&arg){
    return _bind_obj<std::decay_t<F>, std::decay_t<A>>(
        std::forward<F>(func),
        std::forward<A>(arg)
    );
}

Fourth, your result is only invocable on a non-const object. But if the callable I'm bind_front()-ing has a const operator(), I should be able to invoke it as const too right?

The same can be said for ref-qualifiers: if I have a function object with &- and &&-qualified overloads, bind_front should respect that.

The solution is actually to write four overloads of operator():

template <class... Args>
inline std::invoke_result_t<F&, A, Args...> operator()(Args&&... a) &;

template <class... Args>
inline std::invoke_result_t<F const&, A, Args...> operator()(Args&&... a) const&;

template <class... Args>
inline std::invoke_result_t<F, A, Args...> operator()(Args&&... a) &&;

template <class... Args>
inline std::invoke_result_t<F const, A, Args...> operator()(Args&&... a) const &&;

Note that I'm also adjusting the type of F in the type trait, and be sure to move the callable in the rvalue cases.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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