Does bind() have any advantage (other than compatibility) over C++11 lambdas?
Asked Answered
D

3

14

I'm thinking about migrating my code toward using C++11-style lambdas instead of having binds everywhere. But I'm not sure if it's a good idea or not.

Does using e.g. boost::lambda (or boost::phoenix) have any practical advantage over C++11-style lambdas?

Is it a good idea to move over to lambdas? Should I migrate my code or not?

Dreamland answered 21/8, 2012 at 20:38 Comment(3)
Sometimes boost lambda stuff is just much less to write, and sometimes you dont have to repeat types...Mori
@PlasmaHH: Yes. Readability, compatibility, and polymorphism are the advantages mentioned in a lot of places on SO, when people ask about the advantages of lambdas (like here) or try to compare lambdas and bind. But I posted this question (and answer) to point out that even if you're in a situation where they are both readable and usable (i.e. even if you don't need polymorphism, and even if readability isn't an issue), there are still reasons to choose bind.Dreamland
I don't know about boost's lambdas. But in context of C++11 I heard from Scott Meyers presentation: "Lambdas typically preferable and they typically generate better code. Calls through bind involve function pointers ⇒ no inlining. Calls through closures allow full inlining."Alimony
D
3

Yes: It can (sometimes) significantly affect the output sizes.

If your lambdas are different from each other in any way, they will generate different code, and the compiler will likely not be able to merge the identical parts. (Inlining makes this a lot harder.)

Which doesn't look like a big deal when you first look at it, until you notice:
When you use them inside templated functions like std::sort, the the compiler generates new code for each different lambda.

This can blow up the code size disproportionately.

bind, however, is typically is more resilient to such changes (although not immune to them).

To illustrate what I mean...

  1. Take the example below, compile it with GCC (or Visual C++), and note the output binary size.
  2. Try changing if (false) to if (true), and seeing how the output binary size changed.
  3. Repeat #1 and #2 after commenting out all except one of the stable_sorts in each part.

Notice that the first time, C++11 lambdas are slightly smaller; after that, their size blows up after each use (about 3.3 KB of code for each sort with VC++, similar with GCC), whereas the boost::lambda-based binaries barely change their sizes at all (it stays the same size for me when all four are included, to the nearest half-kilobyte).

#include <algorithm>
#include <string>
#include <vector>
#include <boost/lambda/bind.hpp>
#include <boost/lambda/lambda.hpp>   // can also use boost::phoenix

using namespace boost::lambda;

struct Foo { std::string w, x, y, z; };

int main()
{
    std::vector<Foo> v1;
    std::vector<size_t> v2;
    for (size_t j = 0; j < 5; j++) { v1.push_back(Foo()); }
    for (size_t j = 0; j < v1.size(); j++) { v2.push_back(j); }
    if (true)
    {
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::w, var(v1)[_1]) < bind(&Foo::w, var(v1)[_2]));
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::x, var(v1)[_1]) < bind(&Foo::x, var(v1)[_2]));
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::y, var(v1)[_1]) < bind(&Foo::y, var(v1)[_2]));
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::z, var(v1)[_1]) < bind(&Foo::z, var(v1)[_2]));
    }
    else
    {
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].w < v1[j].w; });
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].x < v1[j].x; });
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].y < v1[j].y; });
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].z < v1[j].z; });
    }
}

Note that this is "trading size for speed"; if you're in a very very tight loop, it can involve an extra variable (because now it's using pointers-to-members).
However, this is nothing like the overhead std::function introduces (which is a virtual call), and even that is unmeasurable in many cases, so this shouldn't be a cause for concern.

Dreamland answered 21/8, 2012 at 20:38 Comment(16)
If your lambdas are different from each other in any way The type of a lambda expression is a unique type, each lambda is different to all other lambdas (where each means a lambda defined in a particular translation unit in a particular line of code)Cooksey
@DavidRodríguez-dribeas: That's what I thought at first, but some people pointed out that the linker can merge identical code, if it's only a type difference. Checking it right now, it seems to be the case with VC++ at least partially (I don't know if it works fully, though); I don't know about GCC.Dreamland
A quick test with VS 2010 seems to indicate that this is not the case (maybe it is driven by some compiler flag, or maybe I am not interpreting the dump of the symbols --dumpbin /symbols-- correctly.Cooksey
@DavidRodríguez-dribeas: Are you compiling one translation unit, or multiple? I'm talking about usages in the same translation unit, and I'm looking at the file size increases, not the symbols. Maybe it's the compiler merging them, not the linker. It seems like with duplicate lambdas, the file sizes increase more than would ideally be the case, but it's still less than when the lambdas are different.Dreamland
My own test. int main() { std::vector<std::function<void(int)>> v; v.push_back([](int i){std::cout<<i<<"\n";}); /*repeated 5 times*/ for (auto it=v.begin();it!=v.end();++it){(*it)(5);}} All in a single translation unit. With 5 lambdas: 79360 bytes in the .exe, with 10 lambdas: 105472Cooksey
I can't compile your example code using GCC 4.7 / clang 3.1 and boost 1.49. The compiler doesn't find a matching operator< for bind(...) < bind(...).Pentastich
@mfontanini: It might be confusing std::bind with boost::lambda::bind. Try qualifying them explicitly.Dreamland
@DavidRodríguez-dribeas: I'm not sure; the difference there isn't big enough for me to tell what is causing it. However, if you take my example and instead of calling stable_sort on the fields x, y, and z, you just call stable_sort on w four times, you should see the file size drop from 26.5 KB to 16.5 KB (or a similar drop). That's a pretty big indication the compiler or linker is merging some stuff (but exactly which parts? I don't know).Dreamland
That was it. Probably ADL was messing things up. C++11 Lambdas -> 35k. Boost.Lambda stuff -> 25k.Pentastich
@mfontanini: It's worth noting, boost::phoenix slightly makes it bigger for me, but probably not worth worrying about.Dreamland
@Mehrdad: Considering that the whole program contains 20 lines and the only difference is the number of lambdas, and that each lambda is a small object (a single call to std::ostream::operator<<(int), the size difference is noticeable.Cooksey
@DavidRodríguez-dribeas: Scary isn't it? :)Dreamland
@Mehrdad: Not particularly scary, just a fact: each lambda defines a unique type, and as such code will be generated for it. Even if the test with VS had shown otherwise (i.e. even if VS was able to merge all equivalent lambdas into one), it would be compiler dependent. The language is quite clear: different lambda definitions generate unique lambda types, which in turn means that if you use it as arguments to other templates that will generate even more code. Additionally, std::function does not require virtual dispatch, and the efficiency can be similar to that of an extra function call.Cooksey
@DavidRodríguez-dribeas: My VS test did show that duplicate lambdas generate less code... I don't know why yours didn't seem to. But yes, it's obviously implementation-dependent... code size is always implementation-dependent to begin with. Anyway, how does std::function not require virtual dispatch? How do you think it calls lambdas?Dreamland
@Mehrdad: That is what I thought when I first heard it. Performing type erasure by creating a hierarchy where the base has a virtual exec (choose your name for the virtual function here) is one option, but not the only one. Another option is creating instead of a base template a function template. That template takes the functor as argument and executes operator() on it, adding a single extra non-virtual function call. The std::function object can then store a pointer to the function (there are some details missing in this description, but you can fill those in).Cooksey
let us continue this discussion in chatCooksey
I
10

The main advantage would be polymorphic functors. Currently, C++11 lambdas are monomorphic, i.e., they only take single argument type, whereas bind() allows you to create functors that accept any argumen type as long as the bound functor is callable with it.

#include <functional>

struct X{
  template<class T, class U>
  void operator()(T, U) const{}
};

int main(){
  X x;
  auto l_with_5 = [x](int v){ return x(v, 5); };
  auto b_with_5 = std::bind(x, std::placeholders::_1, 5);
  l(4);
  b("hi"); // can't do that with C++11 lambdas
}
Ila answered 21/8, 2012 at 20:49 Comment(2)
Haha, I was asking the question with the premise that it would be just as easy to use either C++11 lambdas or boost::lambda in the first place, but this is a great point nevertheless, +1.Dreamland
I think "bind is polymorphic" would deserve to be the one accepted, but the trouble is, it's already mentioned in a million other places on StackOverflow, like here. On the other hand, I haven't seen the code-size issue mentioned elsewhere on SO, so since people would likely find out about this anyway, I'm thinking I might accept my own answer instead, if only to bring that to people's attention too, even though this is overall a better answer...Dreamland
F
7

Yes, Boost lambdas are polymorphic, C++11 lambdas are not. That means that, for example, you cannot do it with C++11 lambdas:

template<class T>
void f(T g)
{
    int x = 123;
    const char* y = "hello";
    g(x); // call with an integer
    g(y); // call with a string
}

int main() {
    f(std::cout << _1);
}
Fard answered 21/8, 2012 at 20:50 Comment(1)
Also see this; it applies to your answer as well.Dreamland
D
3

Yes: It can (sometimes) significantly affect the output sizes.

If your lambdas are different from each other in any way, they will generate different code, and the compiler will likely not be able to merge the identical parts. (Inlining makes this a lot harder.)

Which doesn't look like a big deal when you first look at it, until you notice:
When you use them inside templated functions like std::sort, the the compiler generates new code for each different lambda.

This can blow up the code size disproportionately.

bind, however, is typically is more resilient to such changes (although not immune to them).

To illustrate what I mean...

  1. Take the example below, compile it with GCC (or Visual C++), and note the output binary size.
  2. Try changing if (false) to if (true), and seeing how the output binary size changed.
  3. Repeat #1 and #2 after commenting out all except one of the stable_sorts in each part.

Notice that the first time, C++11 lambdas are slightly smaller; after that, their size blows up after each use (about 3.3 KB of code for each sort with VC++, similar with GCC), whereas the boost::lambda-based binaries barely change their sizes at all (it stays the same size for me when all four are included, to the nearest half-kilobyte).

#include <algorithm>
#include <string>
#include <vector>
#include <boost/lambda/bind.hpp>
#include <boost/lambda/lambda.hpp>   // can also use boost::phoenix

using namespace boost::lambda;

struct Foo { std::string w, x, y, z; };

int main()
{
    std::vector<Foo> v1;
    std::vector<size_t> v2;
    for (size_t j = 0; j < 5; j++) { v1.push_back(Foo()); }
    for (size_t j = 0; j < v1.size(); j++) { v2.push_back(j); }
    if (true)
    {
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::w, var(v1)[_1]) < bind(&Foo::w, var(v1)[_2]));
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::x, var(v1)[_1]) < bind(&Foo::x, var(v1)[_2]));
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::y, var(v1)[_1]) < bind(&Foo::y, var(v1)[_2]));
        std::stable_sort(v2.begin(), v2.end(), bind(&Foo::z, var(v1)[_1]) < bind(&Foo::z, var(v1)[_2]));
    }
    else
    {
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].w < v1[j].w; });
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].x < v1[j].x; });
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].y < v1[j].y; });
        std::stable_sort(v2.begin(), v2.end(), [&](size_t i, size_t j) { return v1[i].z < v1[j].z; });
    }
}

Note that this is "trading size for speed"; if you're in a very very tight loop, it can involve an extra variable (because now it's using pointers-to-members).
However, this is nothing like the overhead std::function introduces (which is a virtual call), and even that is unmeasurable in many cases, so this shouldn't be a cause for concern.

Dreamland answered 21/8, 2012 at 20:38 Comment(16)
If your lambdas are different from each other in any way The type of a lambda expression is a unique type, each lambda is different to all other lambdas (where each means a lambda defined in a particular translation unit in a particular line of code)Cooksey
@DavidRodríguez-dribeas: That's what I thought at first, but some people pointed out that the linker can merge identical code, if it's only a type difference. Checking it right now, it seems to be the case with VC++ at least partially (I don't know if it works fully, though); I don't know about GCC.Dreamland
A quick test with VS 2010 seems to indicate that this is not the case (maybe it is driven by some compiler flag, or maybe I am not interpreting the dump of the symbols --dumpbin /symbols-- correctly.Cooksey
@DavidRodríguez-dribeas: Are you compiling one translation unit, or multiple? I'm talking about usages in the same translation unit, and I'm looking at the file size increases, not the symbols. Maybe it's the compiler merging them, not the linker. It seems like with duplicate lambdas, the file sizes increase more than would ideally be the case, but it's still less than when the lambdas are different.Dreamland
My own test. int main() { std::vector<std::function<void(int)>> v; v.push_back([](int i){std::cout<<i<<"\n";}); /*repeated 5 times*/ for (auto it=v.begin();it!=v.end();++it){(*it)(5);}} All in a single translation unit. With 5 lambdas: 79360 bytes in the .exe, with 10 lambdas: 105472Cooksey
I can't compile your example code using GCC 4.7 / clang 3.1 and boost 1.49. The compiler doesn't find a matching operator< for bind(...) < bind(...).Pentastich
@mfontanini: It might be confusing std::bind with boost::lambda::bind. Try qualifying them explicitly.Dreamland
@DavidRodríguez-dribeas: I'm not sure; the difference there isn't big enough for me to tell what is causing it. However, if you take my example and instead of calling stable_sort on the fields x, y, and z, you just call stable_sort on w four times, you should see the file size drop from 26.5 KB to 16.5 KB (or a similar drop). That's a pretty big indication the compiler or linker is merging some stuff (but exactly which parts? I don't know).Dreamland
That was it. Probably ADL was messing things up. C++11 Lambdas -> 35k. Boost.Lambda stuff -> 25k.Pentastich
@mfontanini: It's worth noting, boost::phoenix slightly makes it bigger for me, but probably not worth worrying about.Dreamland
@Mehrdad: Considering that the whole program contains 20 lines and the only difference is the number of lambdas, and that each lambda is a small object (a single call to std::ostream::operator<<(int), the size difference is noticeable.Cooksey
@DavidRodríguez-dribeas: Scary isn't it? :)Dreamland
@Mehrdad: Not particularly scary, just a fact: each lambda defines a unique type, and as such code will be generated for it. Even if the test with VS had shown otherwise (i.e. even if VS was able to merge all equivalent lambdas into one), it would be compiler dependent. The language is quite clear: different lambda definitions generate unique lambda types, which in turn means that if you use it as arguments to other templates that will generate even more code. Additionally, std::function does not require virtual dispatch, and the efficiency can be similar to that of an extra function call.Cooksey
@DavidRodríguez-dribeas: My VS test did show that duplicate lambdas generate less code... I don't know why yours didn't seem to. But yes, it's obviously implementation-dependent... code size is always implementation-dependent to begin with. Anyway, how does std::function not require virtual dispatch? How do you think it calls lambdas?Dreamland
@Mehrdad: That is what I thought when I first heard it. Performing type erasure by creating a hierarchy where the base has a virtual exec (choose your name for the virtual function here) is one option, but not the only one. Another option is creating instead of a base template a function template. That template takes the functor as argument and executes operator() on it, adding a single extra non-virtual function call. The std::function object can then store a pointer to the function (there are some details missing in this description, but you can fill those in).Cooksey
let us continue this discussion in chatCooksey

© 2022 - 2024 — McMap. All rights reserved.