TL;DR
分享某位不愿透露姓名的耿老板发现的 libc++ 的某个奇怪行为:std::function
当内部成员体积足够小,且其 copy 函数标记为 noexcept
时,move ctor 或 assign 函数会优先调用内部成员的 copy 函数,而不是 move 函数。
这不是 bug,但很反直觉。
问题
看下面这段代码,你觉得它的输出该是什么
1 | auto holder = std::make_shared<T>(...); // holder holds some resource |
直觉告诉我应该是 empty,但这是真的吗?
从 Compiler Explorer 我们看到,在不同编译器下,有不同结果:
- clang + libc++:empty
- clang + libstdc++:non-empty
- gcc:non-empty
- msvc:non-empty
说明问题出在 libc++ 的实现上。
影响
下面是为什么耿老板突然对这个行为产生了兴趣。
这个问题的影响是:如果我们依赖 std::function
来控制某个对象的生命期,则在后续 move 这个 std::function
之后,必须要手动 clear 或者析构旧的 std::function
,不能依赖 move 本身的行为。
显然,某些代码不是这么写的。
不是 bug
虽然非常反直觉(毕竟 std::shared_ptr<T>
是 non-trivial 的),但这并不是 bug,因为标准没有规定 move 一个 std::function
之后,旧对象该如何处理:
function( function&& other );
(since C++11)(until C++20) (4)function( function&& other ) noexcept;
(since C++20) (4)3-4) Copies (3) or moves (4) the target of other to the target of *this. If other is empty, *this will be empty after the call too. For (4), other is in a valid but unspecified state after the call. cppreference
“other is in a valid but unspecified state after the call.”
但只有 libc++ 这么做,仍然很让人难受。
libc++
libc++ 里对应的代码在这里。
1 | if (sizeof(_Fun) <= sizeof(__buf_) && |
可以看到,当初始化一个 __value_func
时,如果对应的 _Fp
足够小,且它和它对应的 allocator 的 copy ctor 都是 noexcept
,__value_func
会将 __f_
直接分配在内部 buffer 中。
这里则说的是 __value_func
的 move 函数对于 __f_
直接分配在内部 buffer 的这种情况,直接调用了实际 functor 的 __clone
,但在之后没有对被 move 的对象做任何清理。
1 | if ((void*)__f.__f_ == &__f.__buf_) |
这其实是 libc++ 的一种 SOO(small object optimization),或称 SSO(small string optimization)或 SBO(small buffer optimization)。
std::function copies movable objects when is SOO is used 解释了 libc++ 不想改掉这个行为是因为需要增加 __clone_move
而破坏 ABI 兼容性。
进一步测试
下面这个例子(Compiler Explorer )验证了我们的观点:
1 | struct Test { |
输出为:
1 | Test move true |
Test
和 TestNoExcept
唯一的区别就在于它们 copy ctor 是不是 noexcept
。而这就使得后续的 std::function
的行为产生了区别。真是神奇。
接下来,我们给 TestNoExcept
增加一些体积,使得它不满足 SOO:
1 | struct TestNoExcept { |
输出就变成了:
1 | Test move true |
done。