对一次OOM bug的反思及std::function与move-only可调用对象之间的思考
前言
在编写程序时,很少触发OOM(out of memory)
bug,一般是因为程序中存在逻辑错误或者一些马虎的地方写错了。
而这次触发了OOM
正是因为我自己马虎了,序列化函数的缓冲参数声明类型不是引用且支持拷贝,导致并没有将内容正确写入,然后另一端读取到错误的字段,该字段用于确定std::vector<char>
的大小,调用resize()
分配了超过当前剩余物理内存,从而触发OOM killer
将进程终止了。1
2
3
4
5
6
7
8
9
10
11// buffer passed by value
inline void SerializeField(std::vector<char> const &buf, ChunkList buffer) {
buffer.Append32(buf.size());
buffer.Append(buf.data(), buf.size());
}
inline void SetField(std::vector<char> &buf, Buffer &buffer) {
buf.resize(buffer.Read32()); // Undefined value!
memcpy(&buf[0], buffer.GetReadBegin(), buf.size());
buffer.AdvanceRead(buf.size());
}
反思
通过禁用拷贝构造和拷贝赋值运算符避免隐式拷贝,实现Clone()
和CopyAssign()
以支持显式拷贝。
std::function对move-only可调用对象的陷阱
但是要注意一点,如果该类会被可调用对象(callable)捕获,并由std::function
包裹,那么禁用拷贝特殊成员函数会编译报错即使你是移动std::function
对象。
这种奇妙的现象是因为std::function
使用了type erasure
技巧,而std::function
需要支持拷贝可调用对象,那么就需要用到“虚拷贝函数
”,即所谓的原型模式
(prototype pattern),所以必须需要拷贝函数声明并定义为非删除的
(non-deleted),有关type erasure
的更详细介绍,我打算将来另写一篇文章记录,这里不细展开。
让std::function接受move-only的解决方式
- 声明并定义拷贝特殊成员函数,但是其函数体均为
MY_ASSERT(false)
,其中MY_ASSERT
是release
模式下也能其作用的断言。这样只要只要全部都是移动的话就没有一丝问题,断言会禁止错误的调用,“假的”拷贝特殊拷贝成员可以使编译通过。 - 定义一个仅接受
move-only
可调用对象并只支持移动,在c++23
标准中有符合需求的类:std::move_only_function
,但是通过c++11
也可以实现个类似的,只需要实现个支持move-only可调用对象的std::function
的wrapper就行了:
1 | struct MoveOnly { |
代价只是需要多一次移动,相对来说是可接受的。
Test
1 | struct MoveOnlyFunctor : MoveOnly { |