在C++原生开发中,裸指针既可能指向有效对象,也可能为空,还可能只是借用的引用。这种语义模糊让代码审查和静态检查都很难判定资源归属与空值风险。Guidelines Support Library(简称GSL)是C++核心准则的参考实现,它用极小的开销提供了not_null和owner等工具类型,把约定变成编译器可感知的约束。

一、GSL库与核心准则的背景
C++核心准则(C++ Core Guidelines)是一份由Bjarne Stroustrup和Herb Sutter牵头维护的编程规范,目标是在不牺牲性能的前提下减少常见错误。GSL是该准则的支撑库,包含一系列头文件-only的工具,其中多数类型在运行时几乎没有额外成本,主要价值体现在编译期和静态分析阶段。
传统的裸指针写法如T* p无法表达“这个指针不能为空”或“这个指针负责释放内存”。团队往往只能靠注释约定,一旦人员变动或重构,约定极易被破坏。GSL通过类型系统补全了这部分语义:not_null<T*>表示必然非空,owner<T*>表示拥有资源。二者都不改变指针的本质存储,却让工具和人都能一眼看清意图。
二、not_null的使用方法与原理
not_null是一个模板包装类型,构造时若传入空指针会直接抛出异常或触发断言,从而在程序早期暴露错误。它重载了赋值和解引用操作,保证一旦构造成功,后续使用都不必再做空值判断。
下面是一段对比代码,左侧是普通指针容易埋坑的写法,右侧使用not_null强制约束:
#include <gsl/gsl>
#include <iostream>
// 旧写法:调用方可能传nullptr
void print_length(const char* s) {
if (s == nullptr) return; // 容易遗忘导致崩溃
std::cout << strlen(s) << "n";
}
// 新写法:编译期约束非空
void print_length_safe(gsl::not_null<const char*> s) {
std::cout << strlen(s) << "n"; // 无需判空
}
int main() {
const char* ok = "hello";
print_length_safe(ok); // 正常
// print_length_safe(nullptr); // 编译错误或运行断言
return 0;
}
从原理上看,not_null的构造函数会检查入参。若使用调试构建,通常会调用Expect类断言;若为空则立即终止,避免空指针向后传播。由于它只是薄包装,解引用操作和原生指针性能一致,适合用在函数参数、返回值等边界位置。
需要注意的是,not_null并不能替代智能指针。它只解决“空值”问题,不管理生命周期。若同时需要所有权语义,应结合owner或std::unique_ptr使用。
三、owner的语义与典型场景
owner<T*>是一个标记类型,本身仍是裸指针,但向读者和静态分析器声明:该指针负责对应资源的释放。它不改变运行行为,却能显著减少“谁该delete”的歧义。
在遗留代码迁移时,可以把负责new出来并自行释放的指针显式写成owner:
#include <gsl/gsl>
owner<int*> create_array(std::size_t n) {
return new int[n]; // 明确返回者拥有内存
}
void use_array() {
owner<int*> data = create_array(10);
// 使用data...
delete[] data; // 静态分析可确认配对释放
}
上面的代码中,owner让clang-tidy等工具能追踪分配与释放是否匹配。若某个owner指针在离开作用域前未被释放,分析器会报告泄漏;若非owner指针被delete,也会提示误用。
在容器里存储裸指针时,用owner区分“容器是否拥有元素”也很有效。例如std::vector<owner<Shape*>>表示向量负责销毁各个对象,而std::vector<Shape*>仅保存观察指针。这种写法比注释更可靠。
四、not_null与owner的组合实践
实际接口常需要“既拥有又非空”的指针。可以把二者嵌套:owner<gsl::not_null<T*>>,表示这个指针一定指向有效资源且由当前作用域负责释放。
#include <gsl/gsl>
#include <memory>
struct Buffer {
owner<gsl::not_null<char*>> ptr;
std::size_t len;
Buffer(std::size_t n) : ptr(new char[n]), len(n) {}
~Buffer() { delete[] ptr; }
};
void fill(gsl::not_null<Buffer*> b) {
for (std::size_t i = 0; i < b->len; ++i) b->ptr[i] = 0;
}
这种组合在工厂函数和类成员中很常见:构造时分配并确保非空,析构时释放。由于not_null在构造即校验,若new抛异常则不会进入后续逻辑;而owner提醒维护者必须在析构中清理。
不过嵌套类型会让签名变长,团队可借助别名简化,例如using owned_not_null = owner<gsl::not_null<T*>>;。同时建议在代码评审清单中加入“GSL类型使用核对”,防止退回裸指针老习惯。
五、接入GSL的注意事项
GSL是头文件库,多数环境只需包含gsl/gsl并配置包含路径,无需链接额外二进制。若使用vcpkg或Conan等包管理器,可一键引入。但部分旧编译器对C++14特性支持不全,需确认工具链版本。
另一个常见误区是把not_null用在可能合法为空的返回值上。若接口确实可能无结果,应返回std::optional或std::pair<bool, T*>,而不是强行用not_null导致构造崩溃。GSL类型只是约束手段,不能替代合理的接口设计。
最后,建议配合静态分析。GSL的设计初衷之一是被clang-tidy的cppcoreguidelines规则消费。开启对应检查后,即使开发者忘了写owner,工具也能根据new/delete模式给出警告,形成双重保障。