Why std::bind can’t be (formally) deprecated

Yes: std::bind should be replaced by lambda

For almost all cases, std::bind should be replaced by a lambda expression. It’s idiomatic, and results in better code. There is almost no reason post C++11 to use std::bind.

Doing so is quite straightforward, capture each bind argument by value in the lambda capture list, and provide auto parameters for each of the placeholders, then call the bound callable using std::invoke(). That will handle the cases of member function pointers, as well as regular functions. Now, this is how to do it mechanically, if you were doing this as part of a manual refactoring, the lambda can be made even clearer.

#include <functional>
#include <iostream>

void f(int n1, int n2, int n3) {
  std::cout << n1 << ' ' << n2 << ' ' << n3 << '\n';
}

int main() {
  using namespace std::placeholders;
  int n = 5;
  auto f1 = std::bind(f, 2, n, _1);
  f1(10); // calls f(2, 5, 10);

  auto l1 = [ p1 = 2, p2 = n ](auto _1) { return std::invoke(f, p1, p2, _1); };
  l1(10);

  // idiomatically
  auto l1a = [=](auto _1){return f(2, n, _1);};
  l1a(10);

  auto f2 = std::bind(f, 2, std::cref(n), _1);
  auto l2 = [ p1 = 2, p2 = std::cref(n) ](auto _1) {
      return std::invoke(f, p1, p2, _1);
  };
  // or
  auto l2a = [ p1 = 2, &p2 = n ](auto _1) {
      return std::invoke(f, p1, p2, _1);
  };
  // more idiomatically
  auto l2b = [&](auto _1){f(2, n, _1);};

  n = 7;
  f2(10); // calls f(2, 7, 10);

  l2(10);
  l2a(10);
  l2b(10);
}

Which results in:

2 5 10
2 5 10
2 5 10
2 7 10
2 7 10
2 7 10
2 7 10

No: std::bind provides one thing lambda doesn’t

The expression std::bind evaluates flattens std::bind sub-expressions, and passes the same placeholder parameters down. A nested bind is evaluated with the given parameters, and the result is passed in to the outer bind. So you can have a bind that does something like g( _1, f(_1)), and when you call it with a parameter, that same value will be passed to both g and f. The function g will receive f(_1) as its second parameter.

Now, you could rewrite the whole thing as a lambda, but auto potentially makes this a little more difficult. The result of std::bind is an unutterable type. They weren’t supposed to be naked. However, auto means the expression could be broken down into parts, meaning that the translation from a std::bind expression to a lambda expression is potentially not mechanical. Or, the bind could be part of a template, where the subexpression is a template parameter, which is likely working by accident, rather than design.

In any case, std::bind does not treat its arguments uniformly. It treats a bind expression distinctly differently. At the time, it made some sense. But it makes reasoning about bind expressions difficult.

Don’t do this. But it is why formally deprecating std::bind is difficult. They can be replaced, but not purely mechanically.

There isn’t a simple translation that works, unlike converting from std::auto_ptr to std::unique_ptr, or putting a space after a string where it now looks like a conversion. And, std::bind isn’t broken. It’s sub-optimal because of the complicated machinery to support all of the flexibility, where a lambda allows the compiler to do much better. Also, since the type isn’t utterable, it often ends up in a std::function, which erases the type, removing optimization options.

Example of fail code

#include <functional>
#include <iostream>

void f(int n1, int n2, int n3)
{
    std::cout << n1 << ' ' << n2 << ' ' << n3 << '\n';
}

int g(int n1) { return n1; }

int main()
{
    using namespace std::placeholders;

    auto g1 = std::bind(g, _1);
    auto f2 = std::bind(f, _1, g1, 4);
    f2(10); // calls f(10, g(10), 4);

    // THIS DOES NOT WORK
    // auto l2 = [p1 = g1, p2 = 4](auto _1) {std::invoke(f, _1, p1, p2);};
    // l2(10);

    // The bind translation needs to be composed:
    auto l1 = [](auto _1){return g(_1);};
    auto l2 = [p1 = l1, p2 = 4](auto _1){f(_1, p1(_1), p2); };
    // idiomatically
    auto l2a = [](auto _1) { return f(_1, g(_1), 4);};
    l2(10);
    l2a(10);
}


10 10 4
10 10 4
10 10 4

TODO

If someone can figure out a fixit recommendation that could be safely applied, transforming the old bind to a lambda, then std::bind could be deprecated in C++Next, and removed as soon as C++(Next++). But that right now is non-trivial in some cases.

Updates:

  • Fix incorrect statement about type-erasure in std::bind. I was thinking std::function
  • Add more idiomatic transliterations of the std::bind lambdas

Building and running the examples

Makefile

clean:
    -rm example1
    -rm example2

example1: example1.cpp
    clang++ --std=c++1z example1.cpp -o example1

example2: example2.cpp
    clang++ --std=c++1z example2.cpp -o example2

example3: example3.cpp
    clang++ --std=c++1z example3.cpp -o example3 2>&1

all: example1 example2

Build

rm example1
rm example2
clang++ --std=c++1z example1.cpp -o example1
clang++ --std=c++1z example2.cpp -o example2

Original source

Original document is available on Github

Join the Conversation

5 Comments

    1. You’re right. I was thinking of the std::function that usually, at least in my codebase, ends up holding them. We’ve got an implementation of bind for C++98, and auto isn’t available.

  1. I see your point about mechanical translation, which would definitely be required if std::bind were deprecated. However, I’m afraid your post might give the impression to people with little experience of C++11 (such as myself) that the idiomatic translation of, say,

    auto f1 = std::bind(f, 2, n, _1);

    would be

    auto l1 = [ p1 = 2, p2 = n ](auto _1) { return std::invoke(f, p1, p2, _1); };

    which is not a lot more readable than the std::bind version. If one were to write new code, or to replace std::bind manually while refactoring old code, it would be more reasonable to write:

    auto l1 = [n](auto _1) { return f(2, n, _1) }

    and in the tricky case,

    auto l2 = [](auto _1){ return f(_1, g(_1), 4); };

    Am I mistaken?

    1. No, not mistaken at all. The conversions from std::bind here are purely mechanical, and most of it can, and should be, elided or eliminated if you were to do it manually.

      I should have made that clearer.

  2. std::bind still has a use years later. You can’t (easily) forward a parameter pack to a lambda without jumping through hoops using a tuple.

Leave a comment

Leave a Reply to Steve Downey Cancel reply

Your email address will not be published. Required fields are marked *