前言

在编写程序时,很少触发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_ASSERTrelease模式下也能其作用的断言。这样只要只要全部都是移动的话就没有一丝问题,断言会禁止错误的调用,“假的”拷贝特殊拷贝成员可以使编译通过。
  • 定义一个仅接受move-only可调用对象并只支持移动,在c++23标准中有符合需求的类:std::move_only_function,但是通过c++11也可以实现个类似的,只需要实现个支持move-only可调用对象的std::function的wrapper就行了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct MoveOnly {
MoveOnly() = default;
~MoveOnly() = default;
MoveOnly(MoveOnly const &) = delete;
MoveOnly &operator=(MoveOnly const &) = delete;
MoveOnly(MoveOnly &&) = default;
MoveOnly &operator=(MoveOnly &&) = default;
};

// Don't care of the slice down
// since MoveOnlyFunction is equivalent to the std::function
//
// inherit from the std::function in private
// since I don't think it satisfy the "is a" relationship.
//
//
template<typename F>
class MoveOnlyFunction : private std::function<F>, MoveOnly {
using Base = std::function<F>;

template<typename U>
struct MoveDummy {
U func; // FIXME use EBCO to optimize the empty function object
MoveDummy(U &&f): func(std::move(f)) {}
~MoveDummy() = default;
// Dummy move since copy is invalid
MoveDummy(MoveDummy const &rhs): func(std::move(((MoveDummy&)rhs).func)) { throw -1; }
MoveDummy &operator=(MoveDummy const &) { throw -1; return *this; }
MoveDummy(MoveDummy &&) = default;
MoveDummy &operator=(MoveDummy &&) = default;

template<typename... Args>
auto operator()(Args &&... args) const -> decltype(func(std::forward<Args>(args)...)) {
return func(std::forward<Args>(args)...);
}
};

public:
template<typename U>
MoveOnlyFunction(U &&cb)
: Base(MoveDummy<U>(std::move(cb)))
{}

using Base::operator();
using Base::operator bool;
};

代价只是需要多一次移动,相对来说是可接受的。

Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct MoveOnlyFunctor : MoveOnly {
MoveOnlyFunctor() = default;
void operator()() const {
printf("Move only functor\n");
}
};

int main() {
MoveOnlyFunctor move_only_functor;
MoveOnlyFunction<void()> f(std::move(move_only_functor));
f();

MoveOnlyFunction<void()> h([]() { printf("lambda\n"); });
h();

MoveOnlyFunction<void()> g(std::move(h));
g();
}