I have never seen that idiom used intentionally, I would almost always suspect it to be an accident... but perhaps there are valid use cases in function composition.
The more likely reason it cannot be deprecated... people actively use it and like it.
The point being made here is heavily obfuscated to my eye due to the seemingly needless use of std::invoke when working with the lambda cases (which, even if useful for some reason, is not needed to make the point of this article), but when I finally figured out what is being requested all I could think of is "that functionality of std::bind is horrific and surprising and despite going into this article thinking that of course bind solves a slightly different problem to lambdas in a more readable way, you have now convinced me that std::bind should be deprecated ASAP" (though as it came into existence in the same version of C++ as lambda functions, I mostly end up questioning why it ever existed).
Essentially, in order to provide something that sort of feels like lambda functions, but entirely in templates, the placeholder arguments do not end up locked to a single expression--instead of returning something semantically like a function, std::bind returns something that might be better described as a partial syntax tree that happens to be something you can call as a function, at which point it is only then wrapped up as a function and all subexpression parameters are bound to arguments--and so if you bind something and include in it a bound function, that function is not passed as a value like you would expect: its parameter is hoisted and it becomes a call expression in the new bound composite. The author likes this behavior for some reason, but I can't figure out why it would ever be considered to be a feature and it is trivially simulated with lambdas (while the opposite is not true).
> which is likely working by accident, rather than design.
> At the time, it made some sense. But it makes reasoning about bind expressions difficult. Don’t do this.
> Example of fail code
Where do you get the idea the author likes it?! To me it reads more "it's stupid, but this edge case can't be trivially replaced and thus we can't just rip it out"
I don’t think it’s that the author likes the behavior, but that the author has in mind some rule where features can’t be deprecated unless it’s possible to write a purely mechanical migration tool. Not sure where that rule comes from or why it exists.
The idea is that if you want to deprecate something, you should tell people how to fix their code, so they can get working again with minimal work.
However, even if it could be fixed up, I don't think it will get deprecated.
C++ is (rightly in my mind) extremely cautious about removing things -- it's easy to ignore std::bind, it's just a library function. On the other hand, I'd be annoyed if my perfectly working C++ code base I've been building over 10 years just broke one day. One thing I love about C++ and C is that I can leave my code for 5 or even 10 years, come back, and expect it to still work just fine.
Isn't it what the standard compiler switch is for? For instance if your codebase builds fine with -std=c++17 and std::bind would have been removed in C++20, your code base would still compile fine because the compiler would figure it out.
Unfortunately, that's hard in C++ because lots of code often ends up in headers because templated code must be in headers, and so then you can't link a C++17 and a C++20 library together (for example). Also, it's nice to be able to use new features without having to go and re-write lots of existing code.
It does however fix the basic problem of a complete program.
Hopefully, one day modules will fix this and we'll be able to link modules from different versions of C++ together, but I'm not holding my breath.
Hang on, deprecated != removed. Deprecated just means "do not use in new code", and you'll probably get a diagnostic by default for existing uses, which you might even be able to turn off with a compiler switch.
Also, not being able to create an automated tool to rewrite someone's code is not the same as not being able to tell them how to fix the code themselves.
I originally learned that "deprecated" was meant to indicate that the language no longer promised to include that feature in a future version. So, it still works now, but this is your warning that you should be moving away from it.
Merriam-Webster gives these meanings for "deprecate": "to pray against something, such as an evil (archaic)", "to avert", "to play down" (e.g. one's talent), "to express disapproval of, to disparage".
How this definition is relevant is that the word is used specifically for features that are looked down upon as being harmful in some way, and so using them in programs is creates some kind of risk in addition to "this won't build when it goes away".
The dictionary meaning which comes into play in the software maintenance use of "deprecated" is that "disapprove/disparage" one.
Something that will be removed because it is unfashionable, and being replaced by shiny new idioms, is "obsolescent". Rather than abruptly removing such features, we phase them through an obsolescence period.
Deprecated example: the C gets function (almost impossible to use without creating a security hole, now removed from ISO C).
Obsolescent example: bzero function. (Appears in tradtional BSD sockets networking code, superseded by memset).
The C++ standard defines it like that: "Normative for the current edition of the Standard, but having been identified as a candidate for removal from future revisions. An implementation may declare library names and entities described in this section with the `deprecated` attribute."
> instead of returning something semantically like a function, std::bind returns something that might be better described as a partial syntax tree that happens to be something you can call as a function
Indeed. std::bind seems to work more like a Lisp-ish macro than a function in this regard.
That's a literal translation of what std::bind() is doing that no one would actually write. `[n](auto a) { return f(2, n, a); }` is a more realistic way to write that lambda.
Seeing std::bind tells me that the intention is to create a function that does nothing but call a second function with certain parameters fixed.
A lambda expression is far more general, so when I see it I have to think about what it's doing. There's also a subtle implication that the author chose the more general technique for a reason. It'll raise the questions, "Why isn't this using std::bind()?", "Is there something else going on here?", etc.
Off hand, I can't think of a reason why the bind would be any less efficient than the lambda.
And cleaner still is to not obfuscate things to this degree, no matter what syntax is involved.
It certainly violates the “write once, read many times” principle: no matter how many times I look at something like this, I have to study the whole thing just to figure out what is even happening! Whereas, seeing "void g(x) { f(2, 5, x); }" is perfectly clear.
It certainly violates the “premature optimization” principle: in exchange for some unproven benefit, the compiler does gymnastics to bind arguments and adds compile time instead of just paying a little more in space and runtime to define a 2nd function that calls a 1st function in a completely obvious way.
It also qualifies as over-design. Even if a single binding is considered OK (and that’s a big “if”), there is every reason to expect that during normal maintenance someone will need to keep adding more. Then you have several lines of template-ese. A simple function is something that anyone can extend, whereas it’s not clear most engineers will even know how to maintain the binding version.
Am I misunderstanding or does this example from the post:
auto g1 = std::bind(g, _1);
auto f2 = std::bind(f, _1, g1, 4);
f2(10); // calls f(10, g(10), 4);
Imply that function values in a bind's argument list are silently converted into function invocations? Doesn't this mean you can't pass functions around? Why would you break fundamental assumptions about the type system like this?
The way placeholder values seem to silently propagate across nested binds is also super gross. This stuff doesn't seem composable at all and it seems like it shouldn't have made it into any standard.
No, if I understand your question. You can pass a function pointer or function object via bind, it’s just that the behaviour may be surprising if it’s the result of a bind.
Bind is a stripped down version of boost.lambda [1] which is a generalized DSL for arbitrary expressions. Boost.lambda had, among other things, ways to protected nested binds calls and introduce new scopes for placeholders.
Adding special magic for nested binds (which is used about 0% of the time) was, in hindsight, a big mistake, but when bind was designed and added to the standard, lambda expressions were many years away, so any syntactic sugar that would ease the pain of higher order programming was very welcome.
[1] Although I don't recall who came first. Likely boost.bind was first, then boost.lambda improved on it and some of the improvements were fed back into bind. In fact boost.bind still has some features that std::bind doesn't have (and viceversa).
31 comments
[ 3.9 ms ] story [ 71.1 ms ] threadThe more likely reason it cannot be deprecated... people actively use it and like it.
In either case, nice find.
Essentially, in order to provide something that sort of feels like lambda functions, but entirely in templates, the placeholder arguments do not end up locked to a single expression--instead of returning something semantically like a function, std::bind returns something that might be better described as a partial syntax tree that happens to be something you can call as a function, at which point it is only then wrapped up as a function and all subexpression parameters are bound to arguments--and so if you bind something and include in it a bound function, that function is not passed as a value like you would expect: its parameter is hoisted and it becomes a call expression in the new bound composite. The author likes this behavior for some reason, but I can't figure out why it would ever be considered to be a feature and it is trivially simulated with lambdas (while the opposite is not true).
> which is likely working by accident, rather than design.
> At the time, it made some sense. But it makes reasoning about bind expressions difficult. Don’t do this.
> Example of fail code
Where do you get the idea the author likes it?! To me it reads more "it's stupid, but this edge case can't be trivially replaced and thus we can't just rip it out"
However, even if it could be fixed up, I don't think it will get deprecated.
C++ is (rightly in my mind) extremely cautious about removing things -- it's easy to ignore std::bind, it's just a library function. On the other hand, I'd be annoyed if my perfectly working C++ code base I've been building over 10 years just broke one day. One thing I love about C++ and C is that I can leave my code for 5 or even 10 years, come back, and expect it to still work just fine.
It does however fix the basic problem of a complete program.
Hopefully, one day modules will fix this and we'll be able to link modules from different versions of C++ together, but I'm not holding my breath.
Also, not being able to create an automated tool to rewrite someone's code is not the same as not being able to tell them how to fix the code themselves.
I originally learned that "deprecated" was meant to indicate that the language no longer promised to include that feature in a future version. So, it still works now, but this is your warning that you should be moving away from it.
The dictionary meaning which comes into play in the software maintenance use of "deprecated" is that "disapprove/disparage" one.
Something that will be removed because it is unfashionable, and being replaced by shiny new idioms, is "obsolescent". Rather than abruptly removing such features, we phase them through an obsolescence period.
Deprecated example: the C gets function (almost impossible to use without creating a security hole, now removed from ISO C).
Obsolescent example: bzero function. (Appears in tradtional BSD sockets networking code, superseded by memset).
It's a black stain on any code which uses it, old or new.
A neutral word for "going away" is "obsolescent".
Once made a mistake using std::bind and got 500 lines of errors.
Indeed. std::bind seems to work more like a Lisp-ish macro than a function in this regard.
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);
It seems to me std::bind is a lot cleaner syntactically.
Seeing std::bind tells me that the intention is to create a function that does nothing but call a second function with certain parameters fixed.
A lambda expression is far more general, so when I see it I have to think about what it's doing. There's also a subtle implication that the author chose the more general technique for a reason. It'll raise the questions, "Why isn't this using std::bind()?", "Is there something else going on here?", etc.
Off hand, I can't think of a reason why the bind would be any less efficient than the lambda.
It certainly violates the “write once, read many times” principle: no matter how many times I look at something like this, I have to study the whole thing just to figure out what is even happening! Whereas, seeing "void g(x) { f(2, 5, x); }" is perfectly clear.
It certainly violates the “premature optimization” principle: in exchange for some unproven benefit, the compiler does gymnastics to bind arguments and adds compile time instead of just paying a little more in space and runtime to define a 2nd function that calls a 1st function in a completely obvious way.
It also qualifies as over-design. Even if a single binding is considered OK (and that’s a big “if”), there is every reason to expect that during normal maintenance someone will need to keep adding more. Then you have several lines of template-ese. A simple function is something that anyone can extend, whereas it’s not clear most engineers will even know how to maintain the binding version.
Bind doesn't have to type erase nor in fact does on any of the implementations I'm familiar of.
The way placeholder values seem to silently propagate across nested binds is also super gross. This stuff doesn't seem composable at all and it seems like it shouldn't have made it into any standard.
Adding special magic for nested binds (which is used about 0% of the time) was, in hindsight, a big mistake, but when bind was designed and added to the standard, lambda expressions were many years away, so any syntactic sugar that would ease the pain of higher order programming was very welcome.
[1] Although I don't recall who came first. Likely boost.bind was first, then boost.lambda improved on it and some of the improvements were fed back into bind. In fact boost.bind still has some features that std::bind doesn't have (and viceversa).