Search code examples
c++algorithmboosttransformstd

boost::transform vs std::transform


From the snippet below should I conclude that std::transform is to be preferred over boost::transform as probably more efficient because the former uses fewer initialisations and destructors than the latter?

#include <algorithm>
#include <boost/range/algorithm.hpp>
class Ftor {
public:
    Ftor(const Ftor& rhs) : t(rhs.t)
    { std::cout << " Ftor : copy\n"; }
    Ftor(float rate) : t(rate)
    { std::cout << " Ftor : init\n"; }
    ~Ftor()
    { std::cout << "~Ftor : ...\n"; }
    float operator() (float x) { return x < t ? 0.0 : 1.0; }
private:
    float t;
};

typedef std::vector<float> vec_t;

int main (void) 
{
    vec_t arg(/*...*/);
    vec_t val(arg.size());
    float x = 1.0;
    /* Standard transform test */
    std::cout << "Standard transform:\n";
    std::transform(arg.begin(), arg.end(), val.begin(), Ftor(x));
    std::cout << "Boost transform:\n";
    /* Boost transform test */
    boost::transform(boost::make_iterator_range(arg.begin(), arg.end()),
                     val.begin(), Ftor(x));
}

Output:

Standard transform:
 Ftor : init
~Ftor : ...
Boost transform:
 Ftor : init
 Ftor : copy
~Ftor : ...
~Ftor : ...

Standard transform uses 2 calls. Boost transform uses 4 calls. Standard transform wins. Or does it...?

ADDENDUM

As @sehe suggested std::ref saves one constructor per call to transform with which boost::transform uses only one call. But std::ref cannot take a temporary as argument. But then, passing Ftor f(x) is fine since the latter has a definite address.

Accounting for the constructor/destructor calls when transform is called inside a loop. I've two options with boost now:

std::cout << "with std::ref\n";
for (/*...*/) {
    x = ...;
    f = Ftor(x);
    boost::transform(arg, val.begin(), std::ref(f));
}

std::cout << "with temporary\n";
for (/*...*/) {
    x = ...;
    boost::transform(arg, val.begin(), Ftor(x));
}

Output:

with std::ref
 Ftor : init
 Ftor : init
 ...
~Ftor : ...
with temporary
 Ftor : init
 Ftor : copy
~Ftor : ...
~Ftor : ...
 Ftor : init
 Ftor : copy
~Ftor : ...
~Ftor : ...
 ...

I've got a gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 with or without -O3 yields the same results.

Whether the constructor/destructor is "expensive" is relative to the operator(). It a final product, it is going to perform not too demanding math, not dissimilar to the example above.

Complete example on coliru


Solution

  • STL algorithms are allowed to copy their predicate/functors. This mostly happens because they're passed by value.

    What you see is that boost does one more forward call.

    Is there a problem?

    Not usually. Compilers are very good at inlining, copy elision and dependency analysis.

    Chances are, the code generated ends up being exactly the same.

    Of course, adding the cout statements completely wrecks that. Compare generated code without the constructor/destructor side-effects spoiling it!

    Fair comparison using no-side-effects generates identical code for the STL and Boost variants: http://paste.ubuntu.com/14544891/

    Fix

    The way STL (and boost algorithms) was designed, you can pass the functor by reference explicitly, if you want. You use std::ref for this:

    Live On Coliru

    #include <algorithm>
    #include <vector>
    #include <iostream>
    #include <functional>
    #include <boost/range/algorithm.hpp>
    
    class Ftor {
      public:
        Ftor(const Ftor &rhs) : t(rhs.t) { std::cout << " Ftor : copy" << std::endl; } 
        Ftor(float rate) : t(rate)       { std::cout << " Ftor : init" << std::endl; } 
        ~Ftor()                          { std::cout << "~Ftor : ..."  << std::endl; } 
        float operator()(float x)        { return x; }  
    
      private:
        float t;
    };
    
    typedef std::vector<float> vec_t;
    
    int main(void) {
        vec_t arg(190, 1), val(arg.size());
    
        {
            std::cout << "STL transform: " << std::endl;
            Ftor f(1.0);
            std::transform(arg.begin(), arg.end(), val.begin(), std::ref(f));
        }
        std::cout << "-----\n";
    
        {
            std::cout << "Boost transform: " << std::endl;
            Ftor f(1.0);
            boost::transform(arg, val.begin(), std::ref(f));
        }
        std::cout << "-----\n";
    }
    

    Prints

    STL transform: 
    Ftor : init
    ~Ftor : ...
    -----
    Boost transform: 
    Ftor : init
    ~Ftor : ...
    -----
    

    NOTE Regardless of everything it's very very ironic to use a range algorithm on a container and construct a range boost::make_iterator_range(arg.begin(), arg.end()) instead of just using arg:

    boost::transform(arg, val.begin(), Ftor(x));