C++ 太繁琐了,功能太多, Follow LevelDB, 它只使用很少的好的 C++ 特性, 可以当作 C with Class 升级版本.
在基本对象设计方面,LevelDB 对 struct 和 class 的使用遵循清晰的原则。struct 用于纯数据聚合 (POD-like):
struct LEVELDB_EXPORT Options {
// 默认构造所有字段,纯数据结构不用写太复杂的构造函数
Options();
...
};
Options、ReadOptions、WriteOptions 都使用 struct,因为它们本质上是配置参数的集合,所有成员默认 public,允许自由访问和复制。
struct ManualCompaction {
int level;
bool done;
const InternalKey* begin; // null = 范围起点
const InternalKey* end; // null = 范围终点
InternalKey tmp_storage; // 压缩进度记录
};
内部数据结构如 ManualCompaction、CompactionStats 也使用 struct。
而 class 则用于有行为或不变式的类型:
DB() = default;
DB(const DB&) = delete;
DB& operator=(const DB&) = delete;
virtual ~DB();
DB、Cache、Iterator 等需要封装、有资源管理需求的类型使用 class。
LevelDB 中大量类禁止复制,这是 C++ 资源管理的核心模式。标准写法 (C++11) 如下:
Table(const Table&) = delete;
Table& operator=(const Table&) = delete;
禁止复制的原因主要有三点:一是资源所有权唯一性,如 Arena 管理内存块,PosixSequentialFile 持有文件描述符;二是避免浅拷贝问题,防止多个对象指向同一资源导致 double-free;三是语义不适合复制,如 Env 是全局环境、DB 是数据库句柄。
Arena(const Arena&) = delete;
Arena& operator=(const Arena&) = delete;
MutexLock(const MutexLock&) = delete;
MutexLock& operator=(const MutexLock&) = delete;
但有些类型应该允许复制:
// Slice 只是个指针+长度的 wrapper,拷贝一下无妨
Slice(const Slice&) = default;
Slice& operator=(const Slice&) = default;
Slice 只是指针+长度的轻量级视图,复制开销小且语义正确。
// WriteBatch 也是值语义,随便拷
WriteBatch(const WriteBatch&) = default;
WriteBatch& operator=(const WriteBatch&) = default;
在析构函数与资源管理方面,LevelDB 严格遵循 RAII 原则:资源在构造时获取,在析构时释放。
Arena::~Arena() {
for (size_t i = 0; i < blocks_.size(); i++) {
delete[] blocks_[i];
}
}
Arena 析构时释放所有分配的内存块。
PosixSequentialFile(std::string filename, int fd)
: fd_(fd), filename_(std::move(filename)) {}
~PosixSequentialFile() override { close(fd_); }
文件类在析构时关闭文件描述符。
Status 类实现了自定义的复制和移动语义:
// 默认构造为 OK 状态,无内存分配,高效
Status() noexcept : state_(nullptr) {}
~Status() { delete[] state_; }
Status(const Status& rhs);
Status& operator=(const Status& rhs);
Status(Status&& rhs) noexcept : state_(rhs.state_) { rhs.state_ = nullptr; }
inline Status::Status(const Status& rhs) {
state_ = (rhs.state_ == nullptr) ? nullptr : CopyState(rhs.state_);
}
inline Status& Status::operator=(const Status& rhs) {
// 处理了自赋值 (this == &rhs)
// 也处理了 rhs 和 *this 都有值的情况 -> 先释放旧的,再拷新的
}
inline Status& Status::operator=(Status&& rhs) noexcept {
std::swap(state_, rhs.state_);
return *this;
}
注意 operator= 中检查 state_ != rhs.state_ 同时处理了自赋值和两者都是 OK 的情况。
在接口设计方面,LevelDB 广泛使用抽象基类定义接口,配合工厂模式使用:
class LEVELDB_EXPORT Comparator {
public:
virtual ~Comparator();
// 三路比较: <0, ==0, >0
// 标准的 C 风格比较返回值
virtual int Compare(const Slice& a, const Slice& b) const = 0;
通过工厂函数返回具体实现:
// 内置的字节序比较器
// 注意:返回的单例由模块管理,千万别手欠 delete 它
LEVELDB_EXPORT const Comparator* BytewiseComparator();
// 创建固定大小的 LRU Cache
// 实现了标准的最近最少使用算法
LEVELDB_EXPORT Cache* NewLRUCache(size_t capacity);
LevelDB 使用 NoDestructor 实现安全的单例模式:
template <typename InstanceType>
class NoDestructor {
public:
template <typename... ConstructorArgTypes>
explicit NoDestructor(ConstructorArgTypes&&... constructor_args) {
// static_assert 确保 storage 大小和对齐满足要求 ...
new (instance_storage_)
InstanceType(std::forward<ConstructorArgTypes>(constructor_args)...);
}
~NoDestructor() = default; // 故意不析构,避免静态销毁顺序问题
InstanceType* get() {
return reinterpret_cast<InstanceType*>(&instance_storage_);
}
private:
alignas(InstanceType) char instance_storage_[sizeof(InstanceType)];
};
使用示例:
const Comparator* BytewiseComparator() {
static NoDestructor<BytewiseComparatorImpl> singleton;
return singleton.get();
}
更复杂的单例实现是 SingletonEnv,思路与 NoDestructor 一致,同样使用 aligned storage + placement new,额外在 Debug 模式下记录初始化状态用于断言检查:
Env* Env::Default() {
static PosixDefaultEnv env_container; // SingletonEnv<PosixEnv> 的 wrapper
return env_container.env();
}
Table 类使用 PIMPL 模式 (Pointer to Implementation),通过前向声明的 Rep 结构隐藏实现细节:
private:
struct Rep; // 前向声明,头文件不暴露实现细节
explicit Table(Rep* rep) : rep_(rep) {}
Rep* const rep_; // 所有实现细节藏在 Rep 里
RAII 锁管理是另一个重要的设计模式:
class SCOPED_LOCKABLE MutexLock {
public:
explicit MutexLock(port::Mutex* mu) EXCLUSIVE_LOCK_FUNCTION(mu) : mu_(mu) {
this->mu_->Lock();
}
~MutexLock() UNLOCK_FUNCTION() { this->mu_->Unlock(); }
MutexLock(const MutexLock&) = delete;
MutexLock& operator=(const MutexLock&) = delete;
private:
port::Mutex* const mu_;
};
使用时自动加锁解锁:
void LRUCache::Release(Cache::Handle* handle) {
MutexLock l(&mutex_);
Unref(reinterpret_cast<LRUHandle*>(handle));
}
在内存管理方面,Arena 是 LevelDB 中最重要的内存管理组件,实现了高效的批量分配:
class Arena {
public:
Arena();
~Arena();
Arena(const Arena&) = delete;
Arena& operator=(const Arena&) = delete;
char* Allocate(size_t bytes); // 非对齐分配
char* AllocateAligned(size_t bytes); // 对齐分配 (通常 8 或 16 字节)
size_t MemoryUsage() const {
return memory_usage_.load(std::memory_order_relaxed);
}
private:
char* AllocateFallback(size_t bytes);
char* AllocateNewBlock(size_t block_bytes);
char* alloc_ptr_; // 当前块的分配指针
size_t alloc_bytes_remaining_; // 当前块剩余空间
std::vector<char*> blocks_; // 所有已分配的内存块
std::atomic<size_t> memory_usage_; // 总内存占用(原子变量,但 Arena 通常单线程使用)
};
Arena 的快速路径分配使用了 inline 优化:
inline char* Arena::Allocate(size_t bytes) {
// 0 字节分配语义不明,直接禁止。
// 内部代码保证了不会传 0 进来。
assert(bytes > 0);
if (bytes <= alloc_bytes_remaining_) {
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
return AllocateFallback(bytes);
}
Arena 实现了智能的分配策略:
char* Arena::AllocateFallback(size_t bytes) {
if (bytes > kBlockSize / 4) {
// 超过 1/4 块大小的对象单独分配
// 避免在标准块里留下无法利用的大碎片
char* result = AllocateNewBlock(bytes);
return result;
}
// 当前块剩下的那点空间不够了,直接废弃,开新块
// 典型的空间换时间策略
alloc_ptr_ = AllocateNewBlock(kBlockSize);
alloc_bytes_remaining_ = kBlockSize;
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
大于 1/4 块大小的请求单独分配,避免浪费。
Placement New 技术在 Arena 上构造对象时被广泛使用:
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
const Key& key, int height) {
char* const node_memory = arena_->AllocateAligned(
sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
return new (node_memory) Node(key);
}
LRU Cache 中实现了精细的引用计数管理:
void LRUCache::Ref(LRUHandle* e) {
if (e->refs == 1 && e->in_cache) { // 之前只是在 LRU 热度不够,现在又有人用了,复活!移回 in_use_
LRU_Remove(e);
LRU_Append(&in_use_, e);
}
e->refs++;
}
void LRUCache::Unref(LRUHandle* e) {
assert(e->refs > 0);
e->refs--;
if (e->refs == 0) { // 即使在 Cache 里也被踢了,引用归零 -> 真正释放内存
assert(!e->in_cache);
(*e->deleter)(e->key(), e->value);
free(e);
} else if (e->in_cache && e->refs == 1) {
// 还有 Cache 引用(refs==1),但外部不用了 -> 放入 LRU 列表等待淘汰
LRU_Remove(e);
LRU_Append(&lru_, e);
}
}
资源限制器 (Limiter) 实现了信号量机制,用于限制最大并发资源数:
class Limiter {
public:
Limiter(int max_acquires) : acquires_allowed_(max_acquires) {}
// 尝试获取资源,原子递减;失败则回退递增
bool Acquire() {
int old = acquires_allowed_.fetch_sub(1, std::memory_order_relaxed);
if (old > 0) return true;
acquires_allowed_.fetch_add(1, std::memory_order_relaxed);
return false;
}
void Release() {
acquires_allowed_.fetch_add(1, std::memory_order_relaxed);
}
private:
std::atomic<int> acquires_allowed_;
};
除了上述核心技术,LevelDB 还展示了许多其他 C++ 技巧。Slice 是一个零拷贝的字符串视图实现:
class LEVELDB_EXPORT Slice {
public:
// 各种轻量构造:从 const char*、std::string 等,均为指针赋值
Slice(const Slice&) = default;
Slice& operator=(const Slice&) = default;
const char* data() const { return data_; }
size_t size() const { return size_; }
// 不拥有所有权,clear 只是重置指针
void clear() { data_ = ""; size_ = 0; }
// 高效的前缀移除,只调整指针和长度
void remove_prefix(size_t n) { data_ += n; size_ -= n; }
// 显式深拷贝
std::string ToString() const { return std::string(data_, size_); }
bool starts_with(const Slice& x) const {
return ((size_ >= x.size_) && (memcmp(data_, x.data_, x.size_) == 0));
}
private:
const char* data_;
size_t size_;
};
Slice 的核心思想是不拥有数据,只是视图,类似 C++17 的 std::string_view。
LevelDB 整个项目不使用 C++ 异常,而是通过 Status 类在返回值中携带错误信息。这是 Google C++ 代码的惯例——异常会引入隐式控制流,增加推理难度,且与 RAII 之外的资源管理模式配合不佳。Status 让错误处理变得显式:调用方必须检查返回值,错误不会被悄悄吞掉。
private:
enum Code { kOk = 0, kNotFound = 1, kCorruption = 2,
kNotSupported = 3, kInvalidArgument = 4, kIOError = 5 };
// OK -> state_ 为 nullptr,零开销
// Error -> state_ 指向堆上数组 [4字节长度, 1字节Code, Message...]
const char* state_;
内存布局上,OK 状态不分配任何内存(state_ == nullptr),而错误信息紧凑地打包在一个 char[] 中,避免了额外的 std::string 开销。
LevelDB 实现了跨平台的字节序编码 (Endian-neutral):
inline void EncodeFixed32(char* dst, uint32_t value) {
uint8_t* const buffer = reinterpret_cast<uint8_t*>(dst);
// 现代编译器能识别这种模式,直接优化成 mov/str 指令(处理字节序)
buffer[0] = static_cast<uint8_t>(value);
buffer[1] = static_cast<uint8_t>(value >> 8);
buffer[2] = static_cast<uint8_t>(value >> 16);
buffer[3] = static_cast<uint8_t>(value >> 24);
}
这是小端序编码,现代编译器会优化为单条指令。
线程安全标注帮助静态分析工具检测竞态条件:
// 互斥锁保护以下成员变量
port::Mutex mutex_;
std::atomic<bool> shutting_down_;
port::CondVar background_work_finished_signal_ GUARDED_BY(mutex_);
MemTable* mem_;
MemTable* imm_ GUARDED_BY(mutex_); // 正在 compact 的 immutable memtable
std::atomic<bool> has_imm_; // 允许后台线程在无锁情况下检查是否有 imm_
使用 GUARDED_BY、EXCLUSIVE_LOCKS_REQUIRED 等标注帮助静态分析工具检测竞态条件。
变长数组技巧通过柔性数组成员实现:
struct LRUHandle {
void* value;
void (*deleter)(const Slice&, void* value);
LRUHandle* next_hash;
LRUHandle* next;
LRUHandle* prev;
size_t charge;
size_t key_length;
bool in_cache;
uint32_t refs; // 引用计数
uint32_t hash; // 预存 Hash,避免重复计算
char key_data[1]; // 柔性数组起始位置
Slice key() const { return Slice(key_data, key_length); }
};
通过 char key_data[1] 实现柔性数组成员:
LRUHandle* e =
reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
原子操作与内存序的精细控制在 SkipList 中得到了充分展示:
Node* Next(int n) {
// Acquire Load: 确保读到的节点内容已完整初始化
return next_[n].load(std::memory_order_acquire);
}
void SetNext(int n, Node* x) {
// Release Store: 确保节点初始化对其他线程可见
next_[n].store(x, std::memory_order_release);
}
// 少数安全场景下可用 relaxed 版本,省去屏障开销
Node* NoBarrier_Next(int n) { return next_[n].load(std::memory_order_relaxed); }
void NoBarrier_SetNext(int n, Node* x) { next_[n].store(x, std::memory_order_relaxed); }
SkipList 展示了精细的内存序控制,在性能和正确性之间取得平衡。
总的来说,LevelDB 的代码展示了现代 C++ 的最佳实践。使用 = delete 禁止复制来管理资源所有权,通过 RAII 实现自动资源管理,采用抽象接口加工厂模式实现解耦与可扩展性,使用 Arena 内存池进行高效内存分配,通过 Slice 视图实现零拷贝字符串传递,用 Status 实现无异常错误处理,使用 NoDestructor 实现安全单例,以及通过线程安全标注支持静态分析。这些技术共同构成了一个高性能、可维护的 C++ 代码库。