Is `std::function` allowed to move its arguments?
Asked Answered
S

1

19

While working on this question, I noticed that GCC (v4.7)'s implementation of std::function moves its arguments when they are taken by value. The following code shows this behavior:

#include <functional>
#include <iostream>

struct CopyableMovable
{
    CopyableMovable()                        { std::cout << "default" << '\n'; }
    CopyableMovable(CopyableMovable const &) { std::cout << "copy" << '\n'; }
    CopyableMovable(CopyableMovable &&)      { std::cout << "move" << '\n'; }
};

void foo(CopyableMovable cm)
{ }

int main()
{
    typedef std::function<void(CopyableMovable)> byValue;

    byValue fooByValue = foo;

    CopyableMovable cm;
    fooByValue(cm);
}
// outputs: default copy move move

We see here that a copy of cm is performed (which seems reasonable since the byValue's parameter is taken by value), but then there are two moves. Since function is operating on a copy of cm, the fact that it moves its argument can be seen as an unimportant implementation detail. However, this behavior causes some trouble when using function together with bind:

#include <functional>
#include <iostream>

struct MoveTracker
{
    bool hasBeenMovedFrom;

    MoveTracker()
      : hasBeenMovedFrom(false)
    {}
    MoveTracker(MoveTracker const &)
      : hasBeenMovedFrom(false)
    {}
    MoveTracker(MoveTracker && other)
      : hasBeenMovedFrom(false)
    {
        if (other.hasBeenMovedFrom)
        {
            std::cout << "already moved!" << '\n';
        }
        else
        {
            other.hasBeenMovedFrom = true;
        }
    }
};

void foo(MoveTracker, MoveTracker) {}

int main()
{
    using namespace std::placeholders;
    std::function<void(MoveTracker)> func = std::bind(foo, _1, _1);
    MoveTracker obj;
    func(obj); // prints "already moved!"
}

Is this behavior allowed by the standard? Is std::function allowed to move its arguments? And if so, is it normal that we can convert the wrapper returned by bind into a std::function with by-value parameters, even though this triggers unexpected behavior when dealing with multiple occurrences of placeholders?

Saraisaraiya answered 4/4, 2012 at 9:33 Comment(4)
It seems to me that the issue is more with placeholders than std::function. Namely, the fact that when creating a tie, move is used from the original argument to both expected outputs.Anglosaxon
Interestingly, Visual C++ 11 compiler prints "default copy move" in the first example and doesn't print "already moved!" in the second. I wonder if this additional move may come from inner workings of std::function and/or perfect forwarding.Abisha
@MatthieuM. Could you elaborate? I'm not very familiar with the implementation of placeholders. If the issue comes from the placeholders, how come the problem does not arise when using auto to deduce the "bind-wrapper" type, instead of using std::function?Saraisaraiya
@LucDanton In the latter case the parameter to the function object (the wrapper) will be an lvalue reference of type MoveTracker& and will be forwarded as an lvalue reference to foo, twice, resulting in two copy construction from those lvalue references.Sacaton
E
18

std::function is specified to pass the supplied arguments to the wrapped function with std::forward. e.g. for std::function<void(MoveTracker)>, the function call operator is equivalent to

void operator(CopyableMovable a)
{
    f(std::forward<CopyableMovable>(a));
}

Since std::forward<T> is equivalent to std::move when T is not a reference type, this accounts for one of the moves in your first example. It's possible that the second comes from having to go through the indirection layers inside std::function.

This then also accounts for the problem you are encountering with using std::bind as the wrapped function: std::bind is also specified to forward its parameters, and in this case it is being passed an rvalue reference resulting from the std::forward call inside std::function. The function call operator of your bind expression is thus forwarding an rvalue reference to each of the arguments. Unfortunately, since you've reused the placeholder, it's an rvalue reference to the same object in both cases, so for movable types whichever is constructed first will move the value, and the second parameter will get an empty shell.

Eluviation answered 4/4, 2012 at 10:59 Comment(1)
Oh, I never realized that std::forward<NotRef> was equivalent to std::move<NotRef>! That explains many things.Saraisaraiya

© 2022 - 2024 — McMap. All rights reserved.