最近读levelDB,发现其中有很多很好的OO实践。我将会一篇篇的整理。

C风格的封装:static

之前工作的时候,项目使用static封装一个symbol(variable或function),编译单元之外,static varible变量不可见。如果编译时遇到了两个不同编译单元中有,1. 具有相同命名的,2. 且均为编译单元外可见的变量,则会有链接错误。

1
2
3
4
5
6
// var.cpp
// 编译 gcc -c var.cpp
// 查看symbol nm var.o
// 0000000000000000 d _ZL10var
// 编译单元之外不可见
static int var;
1
2
3
4
5
6
// var.cpp
// 编译 gcc -c var.cpp
// 查看symbol nm var.o
// 0000000000000000 D var
// 编译单元之外可见
int var;

如果需要在一个编译单元中使用,另一个编译单元定义的全局可见的变量,则需要使用extern关键字。如果不使用extern关键字,则编译器会告诉你为变量未定义。细想下来,这个extern有些多此一举,因为编译器可以在编译生成.o文件时,主动放弃对该变量的解析,等待link阶段时候再在其他的obj文件中寻找是否定义。之所引入了extern关键词,这是为了解决编译阶段的效率,提前预警,要求程序员负责声明一个变量是否为外部,保证能在compile期间报错,绝不在link阶段报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// main.cpp
// 编译 gcc -c main.cpp
// 查看symbol nm main.o
// 000000000000001d T main
// U printf
// U var
// 0000000000000000 T _Z3foov
//
// U 表示未解决的symbol,编译期间暂不报错,留到link期间解决
#include "stdio.h"
void foo() {
printf("%d\n", var);
}
int main() {
foo();
return 0;
}

如果不引用static,其实是很危险的,因为任意一个外部的模块,使用static都可以对内部模块的symbol进行调用,给了外部模块一个打破封装的机会。static就好像画地为牢,将修饰的symbol限制在模块以内,有两个好处:

  • 杜绝了外部模块使用extern调用可能性
  • 如果各个模块都遵守约定,使用static修饰自己模块内的symbol,则杜绝了命名冲突的可能性

internal linkage & external linkage

什么叫做internal linkage & external linkage? 这是针对于编译单元(translation unit)的作用域(scope),如若一个symbol(variable or function)的作用域仅编译单元内可见,则为internal linkage,反之则为external linkage。C++对于各种变量的有缺省的linkage属性的设置,我们也可以通过static/extern改变缺省设置,下面的例子来自于stackoverflow

1
2
3
4
5
6
7
8
9
// in namespace or global scope
int i; // extern by default
const int ci; // static by default
extern const int eci; // explicitly extern
static int si; // explicitly static
// the same goes for functions (but there are no const functions)
int foo(); // extern by default
static int bar(); // explicitly static

C++风格的封装: namespace

Google coding style推荐我们使用namespace解决命名空间,这个tip在levelDB中也得到了很好的实践。探究namespace设计的初衷,namespace的诞生本就是为了解决大型工程的命名冲突的,那么namespace比static强在哪里?

好处在于:

  • static没法修饰一个类型,只能修饰一个变量或者是一个函数(Static only applies to names of objects, functions, and anonymous unions, not to type declarations.)
  • 模版非类型参数(none-type arguments)对一个identifier有external linkage的要求(与C++版本有关)
  • 还有一点微小的好处,就是原来的static关键词已经被赋予了太多的语义,这里将linkage相关的语义剥离出来,从语义上讲更加清晰

从实现的结果上来讲上static和匿名namespace造成的结果,都给声明的变量赋予了编译单元外不可见的属性,相对而言,一个具名的namespace是有external linkage属性的。从具体实现来讲,有的blog说匿名的namespace和static都是internal linakge,有的是说匿名的namespace是external linkage,只是用了变名称的方式(这和重载的原理是类似的),根据我的实验,具体与C++的版本有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// generic.cpp
// gcc -c generic.cpp -std=c++98 编译失败,因为KMaxSize具有internal linkage
// gcc -c generic.cpp -std=c++11 编译成功
namespace {
const int kMaxSize = 10;
}
template <typename T, const int N>
struct Array {
T data[N];
};
Array<int, kMaxSize> data;

我的理解是,namespace是一个创建局部有限制的linkage scope的语法糖,匿名namespace是一个绝对封装的包,而具名namespace则可以有限的向外开放,这比单纯的暴露接口,语义上更加明确,且避免了命名空间。

namespace在LevelDB中的实践使用

以cache距离,cache是levelDB中线程安全的LRU-Cache的实现,属于公共组件。那么要求就是该模块的接口具有external linkage属性。根据高内聚的原则,头文件中应该尽量少的暴露接口,cache.h的文件结构如下:

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
#ifndef STORAGE_LEVELDB_INCLUDE_CACHE_H_
#define STORAGE_LEVELDB_INCLUDE_CACHE_H_
#include <stdint.h>
#include "leveldb/export.h"
#include "leveldb/slice.h"
// leveldb的namespace,所以leveldb基本都是在这个namespace下的
namespace leveldb {
// 前置声明
class LEVELDB_EXPORT Cache;
// 工厂方法,鉴于所有symbol默认都是external linkage,所以这是一个对外暴露的接口
// 该函数是一个自由函数(free function, non-member funtion),应该尽量少的使用自由函数,因为这直接进入了当前命名空间
LEVELDB_EXPORT Cache* NewLRUCache(size_t capacity);
// Cache的接口类
class LEVELDB_EXPORT Cache {
public:
Cache() { }
virtual ~Cache();
// Cache的使用者不需要直到handle的具体实现,所以再次隐藏了实现
// 注意这里并不是前置声明,这里是一个及其别扭,但是细想比较有道理的设计,具体可以阅读我关于levelDB的post
// 使用者通过Cache::Handle使用,避免了命名冲突
struct Handle { };
virtual Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value)) = 0;
// 一些纯虚函数作为接口
virtual Handle* Lookup(const Slice& key) = 0;
...
...
} // end of namespace Cache;
} // end of namespace leveldb

cache的实现中,把所有实现相关的类都放在了一个匿名空间中,该命名空间与嵌套在leveldb下,与leveldb::Cache平级,互不干扰,在cache的实现中,需要引用Cache的部分都以Cache::Handle这样的形式进行引用,消除了命名冲突的隐患。不得不说一句,同一个模块的头文件和实现文件相互不在相同的命名空间,也是够严格的。

levelDB还把一些比较常用的组件都放在了leveldb命名以下的一级命名空间里,比如内存池Arena,数据封装Slice,引用他们的时候,在leveldb这个命名空间以内,可以直接使用。简化了一些操作,但是这也是针对于一些高频,不会出歧义的模块而言的。