首先pb有set_allocated_和release_两个接口,set_allocated_是把一个指针拿过来当作自己对应字段的值,并且析构或重设字段时会delete这个指针。这个接口就可以用来做免拷贝。为了阻止析构时delete掉原始数据,可以调用release_接口,把指针释放出来,同时会清空对应的字段。
免拷贝一个想法就是继承pb,在业务中使用set_allocated,然后在析构函数中调用release_,做到自动释放。但是这有个问题,就是pb修改字段会直接修改原字段。
于是继续找更安全的实现方式。看更底层的接口,发现pb序列化分为两个步骤,一是计算序列化字符串的长度,申请足够空间,二是递归把每个字段写进流或字符串。翻开源码,发现这两个接口都是虚函数,那就可以只在构造函数中做手脚,然后直接替换。可是在具体pb中,这两个接口都别声明了final,无法继续覆盖,这里只能用define魔法把final擦掉。
于是改进版本就是只记录一个指针,然后序列化的时候再将其序列化出来。同时额外申请一个空的同类型pb,用来承载修改的工作。这样借用pb后仍然可以修改少量字段,修改是落在新申请的pb上,不会影响原字段。序列化的时候先序列化原字段再序列化空pb。
这里还用到一个特性是pb反序列化的时候会按顺序解析,相同字段可以重复,后面的会覆盖前面的。但是repeated不行,repeated会append到后面去。
// 擦除final限制
// 这个include必须在所有用到的pb的定义之前,建议放在每个需要用的cpp文件的第一行
#include <google/protobuf/stubs/port.h>
#undef PROTOBUF_FINAL
#define PROTOBUF_FINAL
/*
借用pb,实现免拷贝。要保证序列化时借用的pb还在并且赋值正确。借用后可继续小幅修改非repeated字段,不会影响原pb,但会增加序列化的长度。添加repeated字段将会添加到原pb
repeated字段的后面。
用法:
整个pb:
BorrowPb<Pb> pb(ori_pb);
字段:
pb.set_allocated_pb(new BorrowPb<Pb>(ori_pb));
repeated:
pb.mutable_pbs()->AddAllocated(new BorrowPb<Pb>(ori_pb));
*/
template <class Pb>
class BorrowPb : public Pb {
public:
BorrowPb(const Pb& pb) : Pb(), borrowed_pb_(pb) {}
const Pb& GetBorrowed() const { return borrowed_pb_; }
int GetCachedSize() const override { return Pb::GetCachedSize() + borrowed_pb_.GetCachedSize(); }
size_t ByteSizeLong() const override { return Pb::ByteSizeLong() + borrowed_pb_.ByteSizeLong(); }
::google::protobuf::uint8* InternalSerializeWithCachedSizesToArray(
bool deterministic, ::google::protobuf::uint8* target) const override {
return Pb::InternalSerializeWithCachedSizesToArray(
deterministic, borrowed_pb_.InternalSerializeWithCachedSizesToArray(deterministic, target));
}
protected:
const Pb& borrowed_pb_;
};