在最近的开发中,我使用了智能指针来管理一个对象,但是这个对象需要调用特定的函数完成内存释放。用编程的专业术语来讲,就是为智能指针提供一个自定义的deleter。我很好奇的是,这个自定义资源回收函数是否需要在入口处对参数进行空指针检查呢?

需要自定义deleter的典型场景

下面的代码段展示了使用智能指针来管理使用 new 关键字动态分配的 char 数组,由于在释放数组时需要调用 delete[] ,默认的 delete 关键字就不适用了,所以我们提供了一个匿名函数,对象通过这个匿名函数完成销毁。

std::unique_ptr<char, void(*)(char*)> pBuf(new char[10],
					   [](char* p){delete[] p;});

其实 std::unique_ptr 是支持管理由 new 申请的 数组 的,写法如下:

std::unique_ptr<char[]> buf(new char[10]);

在上面的写法中, std::unique_ptr 会使用 delete[] 完成内存释放,这样就满足了释放数组内存的语法要求。

让我来举个离实际开发更近一些的例子。在使用libevent库时,它使用 evbuffer 结构体来缓存网络通信中的数据,并提供了如下的函数接口来申请和释放 evbuffer

struct evbuffer* evbuffer_new();
void evbuffer_free(struct evbuffer* buf);

我希望能够由智能指针自动管理 evbuffer 的生存期,于是采用了如下的写法:

std::unique_ptr<evbuffer, void(*)(evbuffer*)> pBuf(evbuffer_new(), evbuffer_free);

这样就能够在跳出作用域时由 std::unique_ptr 自动释放 evbuffer

自定义deleter要进行空指针判断吗?

以下面的写法为例,在 std::unique_ptr 中保存的是空指针,那么在跳出作用域时要使用自定义的deleter来释放指针对应的内存,那么deleter(即匿名函数)是否需要在入口进行空指针判断呢?

std::unique_ptr<char, void(*)(char*)> pBuf(nullptr,
					   [](char* p){delete[] p;});

答案是:对于 std::unique_ptr 需要的。 std::unique_ptr 在调用deleter之前会判断所管理的指针是否为空指针,若为空指针,则不会调用deleter。所以 std::unique_ptr 的自定义deleter是不需要在函数入口进行空指针判断的。

可以使用下面的代码段进行验证。我在VS2017中进行测试时,运行结果中没有任何输出,说明匿名函数没有执行。

std::unique_ptr<char, void(*)(char*)> buf(nullptr, [](char* p) {
    if (p) {
	std::cout << "not null" << std::endl;
    }
    else {
	std::cout << "null" << std::endl;
    }
});

另外,C++标准 中的一段话也印证了这点,原文截取如下:

Unlike std::unique_ptr , the deleter of std::shared_ptr is invoked even if the managed pointer is null.

标准中还提到了 std::shared_ptr ,然而不同的是 std::shared_ptr 即使管理的指针为空,也会调用deleter,所以在 std::shared_ptr 的deleter中对入口参数进行空指针检查是很有必要的。

对于 std::shared_ptr 的测试代码如下,我在VS2017中的运行结果输出了“null”,说明匿名函数被调用了,而且入口参数为空指针。

std::shared_ptr<char> buf(nullptr, [](char* p) {
    if (p) {
	std::cout << "not null" << std::endl;
    }
    else {
	std::cout << "null" << std::endl;
    }
});

值得注意的是,在 std::shared_ptr 使用自定义deleter时并不需要在方括号中提供函数类型,这与 std::unique_ptr 不同。

我比较好奇,在查阅并比较了标准后,发现他们在定义和声明的时候就有区别,详细代码比较如下。

template< class T > class shared_ptr;

template<
    class T,
    class Deleter = std::default_delete<T>
> class unique_ptr;

使用 std::shared_ptr 模仿go语言 defer 关键字功能

在golang中有个很方便的关键字 defer 用于在跳出作用域时自动执行一个预先设定好的函数,常常用于进行预订一些清理操作函数,确保在各种情况下申请的资源都能被释放。

以下代码摘自 golang blog ,演示了 defer 关键字的用法。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
	return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
	return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

defer 关键字的这个特性与智能指针所提供的功能很相似,我们可以用 std::unique_ptr 完成类似的功能,示例代码如下。

int test_defer() {
    std::shared_ptr<void> d(nullptr, [](...) {std::cout << ", World" << std::endl; });
    std::cout << "Hello";
    return 1;
}

上面的代码实际上是为一个 void 类型的空指针( nullptr )设置了一个自定义deleter,由于 std::shared_ptr 即使在空指针的情况下也会调用deleter,所以在离开作用域后,预先设定好的匿名函数被调用,预期的输出结果为“Hello, World”。

updated 2020/10/09:
这种 std::shared_ptr 形成的简易 defer 在使用时还是有些限制的:

  1. 已经设定好的 defer 函数是不能取消的,调用 reset 接口或使用 nullptr 替代原有指针都会导致原对象销毁,从而触发 defer 函数调用。换言之,只能将 defer 回调函数提前,而不能将其取消。
  2. 若需要 defer 函数能够取消,可以通过增加一个 bool 变量,在 defer 函数的开始通过检查这个 bool 变量决定是否执行后续的操作。这相当于给 defer 回调函数增加了一个开关,通过调整开关能控制函数逻辑是否执行。需要注意匿名函数(lambda表达式)在捕获这个 bool 变量时需要以引用的方式( & 符号),而不能使用拷贝的方式( = 符号 ),在VS2017中测试结果为使用拷贝方式捕获的变量并没有反映变量值的修改结果。
  3. defer 函数执行时,其形式参数可能存在生存期问题,即由于跳出作用域而被销毁。这其实涉及到对象的析构顺序,所以在使用时一定要仔细考虑参数对象的生存期问题。