MSVC使用memcpy复制lambda时出现异常行为的原因咨询
我需要将std::function存入公司内部实现的vector容器,该容器内部使用memcpy而非拷贝/移动运算符,这要求存入的元素必须是平凡可复制类型。我编写了测试程序复现问题:GCC编译下输出符合预期,但MSVC下运行异常。
测试代码
#include<iostream> #include<functional> #include<cstring> using namespace std; using VAR = void(); struct Abc { std::function<VAR> fn; }; struct Test{ int i; }; void openTest(const Test& a) noexcept { cout<<"In Test"<<a.i<<endl; }; void foo(Abc& abc, const Test& test) { abc.fn = [test]() noexcept{ openTest(test);}; } int main() { Abc arr[2]; for(int i=0;i<2;++i) { Abc abc; memset(&abc,0, sizeof(Abc)); Test test{i*8+1}; foo(abc,test); memcpy(&arr[i],&abc,sizeof(Abc)); // 填充数组时查看结果 arr[i].fn(); } // 填充完成后查看结果 for(int i=0;i<2;++i) arr[i].fn(); }
不同编译器执行结果
GCC(x86-64 gcc 12.2)
In Test1 In Test9 In Test1 In Test9
MSVC(x86 msvc v19.latest)
In Test1 In Test9 In Test9 In Test9
调试发现,第二次循环(i==1)执行memcpy时,会同时更新arr[0].fn和arr[1].fn,请问为何MSVC会出现这种异常行为?
解答
核心原因:未定义行为导致的差异
首先必须明确:std::function不是C++标准定义的平凡可复制类型。根据C++标准,只有平凡可复制类型才能通过memcpy安全复制,否则行为完全未定义——GCC下的“正常输出”只是巧合,并非合法的可预期行为。
MSVC异常表现的具体原因
MSVC的std::function实现采用了小对象优化(SSO):对于体积较小的捕获对象(比如示例中的Test结构体),会直接将捕获数据存储在std::function内部的缓冲区中,而非堆上。但当你用memcpy复制std::function时,跳过了它的拷贝构造函数,破坏了内部的状态管理逻辑(比如引用计数、存储指针的正确性)。
在你的测试代码中,第二次循环的栈对象abc的std::function内部存储可能和arr[0].fn的内部存储存在地址重叠,或者因为未定义行为导致指针指向错误,最终memcpy操作意外覆盖了arr[0].fn的内部数据,使得两个std::function都指向了第二次循环的捕获对象。
可行的解决方案
如果公司的vector必须依赖memcpy存储元素,那么不能直接存储std::function,可以考虑以下替代方案:
- 使用普通函数指针:但仅适用于无状态的函数,无法捕获外部变量;
- 自定义平凡可复制封装类型:手动封装函数指针和平凡可复制的捕获数据,自行管理内存;
- 协调修改公司vector的实现:使其支持非平凡可复制类型的拷贝/移动操作,这是最合规的方案。
内容的提问来源于stack exchange,提问作者abhishek gupta

