1

In C++11, std::function is MoveConstructible, i.e. one can meaningfully invoke std::move on such objects or store them in moveable types. A quandary: what should the following code print?

#include <stdio.h>

#include <functional>
#include <utility>

struct Big {
    char data[1024];
};

int main(int argc, char **argv) {
    Big blob;
    // This bind will trigger small object optimization
    std::function<void()> little = std::bind([]() { printf("little\n"); });
    // This bind will not
    std::function<void()> big = std::bind([](Big const& b) {
            printf("big %c\n", b.data[0]);
        }, blob);
    auto little_moved = std::move(little);
    auto big_moved = std::move(big);

    // After move, one expects the source std::function to be empty
    // (boolean value false)

    printf("Little empty: %d\n", !little);
    printf("Little (moved) empty: %d\n", !little_moved);
    printf("Big empty: %d\n", !big);
    printf("Big (moved) empty: %d\n", !big_moved);

    return 0;
}

Compiled with GCC 4.8, you get this:

linux-dev:nater:/tmp$ g++-4.8 -g -o foo move_function.cc  -std=c++11
linux-dev:nater:/tmp$ ./foo
Little empty: 1
Little (moved) empty: 0
Big empty: 1
Big (moved) empty: 0

The object behaves as expected, invalidating the RHS of a move assignment. However, things are not so clear with clang (Apple LLVM version 6.0):

workbrick:nater:/tmp$ clang++ -g -o foo move_function.cc -std=c++11 -stdlib=libc++
workbrick:nater:/tmp$ ./foo
Little empty: 0
Little (moved) empty: 0
Big empty: 1
Big (moved) empty: 0

Here, the RHS is invalidated (false in a boolean context) after move when the bound parameters are large, but not when bound parameters are small (technically, nonexistent). Examining the implementation of <functional> shipped with Xcode, we see that behavior differs depending on whether the small object optimization has been applied:

template<class _Rp, class ..._ArgTypes>
template <class _Alloc>
function<_Rp(_ArgTypes...)>::function(allocator_arg_t, const _Alloc&,
                                     function&& __f)
{
    if (__f.__f_ == 0)
        __f_ = 0;
    else if (__f.__f_ == (__base*)&__f.__buf_)
    {
        // [nater] in this optimization, __f.__f_ is not invalidate
        __f_ = (__base*)&__buf_;
        __f.__f_->__clone(__f_);
    }
    else
    {
        // [nater] here, the RHS gets invalidated
        __f_ = __f.__f_;
        __f.__f_ = 0;
    }
}

Now, I know that the state of the RHS after move assignment is type-specific, but I am surprised that the behavior of this Standard class is not consistent. Is this really undefined in the spec?

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Nate R.
  • 141
  • 4
  • Indeed, so it does. For posterity: _(6) effects: If !f, *this has no target; otherwise, move-constructs the target of f into the target of *this, leaving f in a valid state with an unspecified value._ Feel free to promote this to an answer & I'll accept it. – Nate R. Jan 21 '15 at 23:56

2 Answers2

1

Shamed into looking this up by dyp's helpful comment, it is indeed undefined behavior whether !func will be true or not after auto foo = std::move(func) (or any other move-from on the object). The relevant text from the C++11 spec:

(6) effects: If !f, *this has no target; otherwise, move-constructs the target of f into the target of *this, leaving f in a valid state with an unspecified value

Another win for undefined unspecified behavior.

Community
  • 1
  • 1
Nate R.
  • 141
  • 4
  • It is a bit strange to see the term "undefined behaviour" in this context. Yes, it is "not defined" if the function object is empty, but no Undefined Behaviour is invoked by move-construction a function (in that way). (UB gives leeway to do anything, whereas the possible behaviours here are restricted by the "valid state".) – dyp Jan 22 '15 at 00:20
  • Well, the semantics of the moved-from `std::function` are undefined even if the state of the object is "valid", are they not? That is, the language in the standard give latitude to library implementors to make invocation of `operator bool` or `operator()` have any of their (valid) effects, and in the case of clang's implementation two behaviors are observed, depending on the type of the wrapped *Callable*. The moved-from object isn't empty in one case and is empty in the other. Perhaps we need a new category for behavior that is not "undefined" but still impossible to know ;) – Nate R. Jan 22 '15 at 00:51
  • 1
    The subtle difference between "undefined behaviour" and "unspecified behaviour" is that the latter is "usually delineated by this [C++] International Standard" [defns.unspecified]. The term "undefined behaviour" suggests that `operator bool` could throw an exception or erase the hard drive in a conforming (but silly) implementation. – dyp Jan 22 '15 at 19:17
0

It is unspecified what value an object has, after moving from it.

Aside from it being some valid value, of course.

There are a few common cases:

  1. The source is untouched, if copying is cheap and cannot throw.
  2. The source is left in default-constructed state.
  3. The source is left with whatever value the target had previously. (It was swapped.)
  4. The source is in some crippled state which can only be safely destroyed and assigned.

Generally, the most useful of the cheapest alternatives is chosen.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118