为何Clang实现std::function移动语义时会复制小对象?
libc++中std::function小对象优化时移动操作执行复制的原因分析
在libc++的std::function实现中,当被类型擦除的函数对象体积足够小、可以放入*SBO(小对象优化)*空间时,移动操作会执行复制而非移动。但并非所有栈内存占用小的对象都适合复制,为什么要选择复制而不是移动?
测试现象
- 使用Clang测试时,
test1()的语义与显式复制的test3()完全一致,通过shared_ptr的引用计数变化可以验证这一点:移动操作后引用计数显著增加,说明底层对象被复制了。 - 使用GCC测试时行为符合预期:移动操作不会增加引用计数,底层对象被正确移动。
标准与实现的矛盾
两种实现均符合C标准要求——std::function要求存储的函数对象必须可复制,且移动后的原对象处于未指定状态。但libc的这种设计让人疑惑:类似逻辑如果套用到std::map上,假设键值都可复制,难道移动std::map时也要生成新副本?这显然不符合移动语义的初衷。而且根据cppreference的说明,此处应当执行移动操作。
示例代码
#include <iostream> #include <memory> #include <functional> #include <array> #include <type_traits> void test1() { /// 用shared_ptr观察内部行为,它的use_count()能直观反映对象的复制/移动情况 auto foo = std::make_shared<int>(0); /// shared_ptr并非平凡可复制类型,复制可能带来额外开销 /// C++23之前没有move_only_function,只能用std::function static_assert(!std::is_trivially_copyable_v<std::decay_t<decltype(foo)>>); static_assert(!std::is_trivially_copy_constructible_v<std::decay_t<decltype(foo)>>); std::cout << std::endl; std::cout << "Test 1: 可放入SBO的小型函数对象" << std::endl; std::cout << "引用计数: " << foo.use_count() << std::endl; std::function<void()> f = [foo] { /// 空操作,仅捕获对象 }; std::cout << "引用计数: " << foo.use_count() << std::endl; { auto g = std::move(f); /// 底层被类型擦除的数据实际是被复制而非移动 std::cout << "引用计数: " << foo.use_count() << std::endl; } std::cout << "引用计数: " << foo.use_count() << std::endl; } void test2() { auto foo = std::make_shared<int>(0); std::cout << std::endl; std::cout << "Test 2: 无法放入SBO的大型函数对象" << std::endl; std::cout << "引用计数: " << foo.use_count() << std::endl; std::function<void()> f = [foo, bar = std::array<char, 1024>()] { (void)bar; /// 空操作,仅捕获对象 }; std::cout << "引用计数: " << foo.use_count() << std::endl; { auto g = std::move(f); std::cout << "引用计数: " << foo.use_count() << std::endl; } std::cout << "引用计数: " << foo.use_count() << std::endl; } void test3() { auto foo = std::make_shared<int>(0); std::cout << std::endl; std::cout << "Test 3: 显式复制小型函数对象" << std::endl; std::cout << "引用计数: " << foo.use_count() << std::endl; std::function<void()> f = [foo] { /// 空操作,仅捕获对象 }; std::cout << "引用计数: " << foo.use_count() << std::endl; { auto g = f; std::cout << "引用计数: " << foo.use_count() << std::endl; } std::cout << "引用计数: " << foo.use_count() << std::endl; } int main() { test1(); test2(); test3(); return 0; }
内容的提问来源于stack exchange,提问作者GreenScape
相关产品推荐
相关产品推荐

