Home > Enterprise >  The reasoning behind clang's implementation of std::function's move semantics
The reasoning behind clang's implementation of std::function's move semantics

Time:01-12

My issue is with how the move semantics is done in libc 's implementation of std::function. I can't understand the logic behind the design choices that were made. Or is this just a bug/oversight?

The issue lies in a single fact: if the function object, whose type is being erased inside of a std::function, is small enough to fit inside of an SBO, then the move operation on std::function object will actually copy(!) the underlying function object not move it. You can imagine that not every object whose stack memory footprint is small is optimal to be copied.

Consider the example with clang (shared_ptr is just used here as a neat tool that has reference counting):

https://wandbox.org/permlink/9oOhjigTtOt9A8Nt

The semantics in a test1() is identical to that of test3() where an explicit copy is used. And shared_ptr helps us to see that.

On the other hand, GCC behaves reasonably and predictably (my subjective view):

https://wandbox.org/permlink/bYUDDr0JFMi8Ord6

And yes, all of that lies inside of the 'gray' area allowed by the standard. std::function requires functions to be copyable, moved-from object is left in unspecified state and so on. My point is: why do that? The same reasoning, probably, may be applied to an std::map: if both the key and value are copyable, then why not make a new copy whenever someone std::moves an std::map? That would also be perfectly within the standard's requirements.

Actually, according to cppreference.com the target should be moved.

CodePudding user response:

It is a bug in libc that cannot be immediately fixed because it would break ABI. Apparently, it is a conforming implementation, although obviously it is often suboptimal.

It's not clear exactly why the Clang devs made such an implementation choice in the first place (although maybe if you're really lucky, someone from Clang will show up and answer this question). It may simply have to do with the fact that Clang's strategy avoids having to have a "vtable" entry for move construction, and thus simplifies the implementation. Also, as I wrote elsewhere, the Clang implementation only uses SOO if the callable is nothrow-copy-constructible in the first place, so it will never use SOO for things that have to allocate from the heap (like a struct that contains a std::vector) so it will never copy such things upon move construction*. That means the practical effect of the cases where it does copy instead of moving is limited (although it will certainly still cause degraded performance in some cases, such as with std::shared_ptr, where a copy operation must use atomic instructions and a move operation is almost free).

* OK, there is a caveat here: if you use the allocator-extended move constructor, and the provided allocator is unequal to the one from the source object, you force the libc implementation to perform a copy, since, in the case of unequal allocators, it can't just take ownership of the pointer to the out-of-line callable held by the source object. However, you shouldn't use the allocator-extended move constructor anyway; allocator support was removed in C 17 because implementations had various issues with it.

  • Related