5

I was experimenting with std::async and ended up with a code that looks like that :

class obj {
public:
    int val;

    obj(int a) : val(a) {
        cout << "new obj" << endl;
    }
    ~obj() {
        cout << "delete obj" << endl;
    }
};


void foo(obj a) {

    this_thread::sleep_for(chrono::milliseconds(500));
    cout << a.val << endl;
}

int main(int argc, int **args) {

    obj a(5);
    auto future = async(foo, a);
    future.wait();

    return 0;
}

and the result is :

new obj
delete obj
delete obj
delete obj
5
delete obj
delete obj
delete obj

I then tried to change void foo(obj a) by void foo(obj &a) :

new obj
delete obj
delete obj
delete obj
5
delete obj
delete obj

Why would 5 copies of my object be made for this simple code? I have to admit, I'm really confused. Would someone care to explain this?

Edit

I'm using VS2012

Simon-Okp
  • 687
  • 7
  • 28
  • 1
    Try to make a copy or move constructor in the `obj` class. – Some programmer dude Feb 16 '13 at 15:58
  • While he is seeing the effects of the copy constructor, it still would be interesting to know _why_ there are 5 additional copies. My simple mind following the code can see three (maybe 4) obvious ones, but at least two are not immediately obvious. – Chad Feb 16 '13 at 16:04
  • 1
    Using `const obj&` as `foo`'s argument on GCC 4.7, I get 3 "delete obj", which means 2 copies are being made. – mfontanini Feb 16 '13 at 16:23
  • @JoachimPileborg the copy constructor is called as many time as there is deletes. But i'm more interested to know why the copy constructor is called that many times, and maybe how to prevent this (it would be quite a waste if it was a bigger object) – Simon-Okp Feb 16 '13 at 16:26
  • Are you compiling using optimizations enabled? – mfontanini Feb 16 '13 at 16:33
  • @mfontanini I'm still getting 5 "delete obj" even with `const obj&`. I'm using VS2012. I tried compiling with or without any optimizations, it didn't change anything. – Simon-Okp Feb 16 '13 at 16:40
  • Try `async(foo, std::ref(a))`. – zch Feb 16 '13 at 16:41
  • Even with the original code there should be no more than three copies, that's what GCC's implementation does. If the type was movable it would be one copy and two moves. With `void foo(obj &a)` the code is invalid, so it's a MSVC bug that it compiles, and it's an MSVC bug that it makes so many copies. – Jonathan Wakely Feb 23 '13 at 14:12

2 Answers2

7

In your case, obj is being copied:

  1. Twice by the call to std::async.
  2. Twice by async's internal call to std::bind.
  3. Once by the call to void foo(obj a) since it takes a by value.

Believe it or not, the number of copies has actually been reduced since VC10.

It is not at all uncommon to see a library (be it the standard library or another one) trigger a few more copies than you would expect on your types. And usually, there is not too much you can do about it in.

There are 2 things that people commonly do to prevent copies:

  1. Take obj by reference (or in your case, const ref since foo does not modify obj). This will require using std::ref with async.
  2. Define a move constructor for obj. This won't prevent temporaries from being constructed and destroyed, but it will give you a chance to optimize the process a bit.

Note that in your bare example of an object that holds onto only one int, it might actually be faster to copy rather than move or pass by reference.


Example for passing obj by reference into async:

void foo(const obj& a) {
    this_thread::sleep_for(chrono::milliseconds(500));
    cout << a.val << endl;
}

int main(int argc, int **args) {
    obj a(5);
    auto future = async(foo, std::cref(a));
    future.wait();

    return 0;
}

Example for defining a move constructor:

class obj
{
public:
    /* ... */

    obj(obj&& a) : val(move(a.val)) {
        // It is good practice to 0 out the moved object to catch use-after-move bugs sooner.
        a.val = 0;
    }

    /* ... */
};
Community
  • 1
  • 1
Sean Cline
  • 6,979
  • 1
  • 37
  • 50
  • as described here http://stackoverflow.com/a/15040860/893819 I think declairing a move constructor can actually stop temporaries from being created. – odinthenerd Feb 23 '13 at 13:25
  • @PorkyBrain, that's not what the answer says. The compiler is allowed to elide copies too, it's not controlled by the presence of a move cosntructor – Jonathan Wakely Feb 23 '13 at 14:03
  • 1
    why std::async copies the object twice? – DoehJohn May 27 '20 at 19:34
  • @DoehJohn It's just how MSVC's implementation of `std::async` works. There will always be at least one copy or move so `async` can have its own copy. Others aren't necessary, but are just part of how `async` passes the arguments around internally. Newer versions of Visual Studio are better about it now that `std::bind` isn't used internally, but there are still a couple extra moves. – Sean Cline May 28 '20 at 17:35
2

a is being copied during the bind phase. To avoid multiple copies of a, use move constructor semantics:

Add a move ctor to obj:

class obj {
public:
    ...
    obj(obj&& other) {
        cout << "move obj" << endl;
        val = std::move(other.val);
    }
};

In main:

    obj a(5);
    auto future = async(foo, std::move(a));
    ...

This way, 5 instances of obj will still be created, but since async supports movable objects, the same copy will be moved from instance to instance (for heavy objects this will be significant over copying the object around). So now the output should be:

new obj
move obj
move obj
move obj
move obj
delete obj
delete obj
delete obj
5
delete obj
delete obj
eladidan
  • 2,634
  • 2
  • 26
  • 39