C++ std::unique_ptr 的自定义删除器
2023-11-215.3k 阅读
一、std::unique_ptr 简介
在 C++ 中,std::unique_ptr
是一种智能指针,它负责管理动态分配的对象的生命周期。std::unique_ptr
具有独占所有权语义,这意味着在任何时刻,只有一个 std::unique_ptr
可以指向给定的对象。当 std::unique_ptr
被销毁时,它所指向的对象也会被自动销毁。
std::unique_ptr
的基本语法如下:
std::unique_ptr<T> ptr(new T());
这里,T
是要管理的对象的类型,new T()
是动态分配一个 T
类型对象的表达式。std::unique_ptr
会在其析构函数中自动调用 delete
来释放该对象。
二、为什么需要自定义删除器
- 非标准内存释放方式
- 有时候,我们动态分配的资源不是通过
new
分配的,例如使用malloc
分配的内存,这时不能简单地使用delete
来释放,而需要调用free
。 - 对于一些外部库提供的资源管理函数,例如
CreateFile
创建的文件句柄,需要使用CloseHandle
来关闭,而不是delete
。
- 有时候,我们动态分配的资源不是通过
- 特殊的清理逻辑
- 当对象包含一些需要特殊清理的资源时,默认的
delete
操作可能不够。例如,一个对象在创建时打开了一个网络连接,在销毁时不仅要释放对象本身的内存,还需要关闭网络连接。
- 当对象包含一些需要特殊清理的资源时,默认的
三、自定义删除器的实现方式
- 函数指针作为删除器
- 我们可以定义一个普通函数作为自定义删除器,然后将该函数指针传递给
std::unique_ptr
的构造函数。 - 下面是一个使用
malloc
和free
管理内存的示例:
- 我们可以定义一个普通函数作为自定义删除器,然后将该函数指针传递给
#include <iostream>
#include <memory>
void customDelete(void* ptr) {
std::cout << "Custom delete function called." << std::endl;
free(ptr);
}
int main() {
std::unique_ptr<char, decltype(&customDelete)> ptr(static_cast<char*>(malloc(100)), customDelete);
if (ptr) {
std::cout << "Memory allocated successfully." << std::endl;
}
return 0;
}
- 在这个例子中,我们定义了
customDelete
函数来释放通过malloc
分配的内存。std::unique_ptr
的第二个模板参数指定了删除器的类型,这里是一个函数指针。构造函数的第二个参数传递了实际的删除器函数customDelete
。
- lambda 表达式作为删除器
- lambda 表达式提供了一种更简洁的方式来定义删除器,尤其是当删除逻辑比较简单时。
- 假设我们有一个表示文件句柄的类,并且需要在对象销毁时关闭文件句柄:
#include <iostream>
#include <memory>
#include <windows.h>
class FileHandle {
public:
FileHandle(const wchar_t* filename) : handle(CreateFileW(filename, GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)) {
if (handle == INVALID_HANDLE_VALUE) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandle() {
if (handle != INVALID_HANDLE_VALUE) {
CloseHandle(handle);
}
}
HANDLE getHandle() const {
return handle;
}
private:
HANDLE handle;
};
int main() {
std::unique_ptr<FileHandle, decltype([](FileHandle* f) {
if (f) {
CloseHandle(f->getHandle());
delete f;
}
})> ptr(new FileHandle(L"test.txt"), [](FileHandle* f) {
if (f) {
CloseHandle(f->getHandle());
delete f;
}
});
return 0;
}
- 在这个例子中,我们使用 lambda 表达式定义了删除器。lambda 表达式捕获了
FileHandle
对象的指针,并在其中关闭文件句柄并释放对象。std::unique_ptr
的第二个模板参数通过decltype
从 lambda 表达式推断出来,构造函数的第二个参数传递了实际的 lambda 表达式。
- 自定义删除器类
- 我们还可以定义一个类,并重载其
()
运算符来实现删除器逻辑。 - 以下是一个管理数据库连接的示例:
- 我们还可以定义一个类,并重载其
#include <iostream>
#include <memory>
// 模拟数据库连接类
class DatabaseConnection {
public:
DatabaseConnection() {
std::cout << "Database connection created." << std::endl;
}
~DatabaseConnection() {
std::cout << "Database connection destroyed." << std::endl;
}
};
// 自定义删除器类
class DatabaseConnectionDeleter {
public:
void operator()(DatabaseConnection* conn) const {
std::cout << "Custom deleter for database connection called." << std::endl;
// 可以在这里添加额外的清理逻辑,例如关闭连接
delete conn;
}
};
int main() {
std::unique_ptr<DatabaseConnection, DatabaseConnectionDeleter> ptr(new DatabaseConnection());
return 0;
}
- 在这个例子中,
DatabaseConnectionDeleter
类重载了()
运算符,实现了数据库连接对象的删除逻辑。std::unique_ptr
的第二个模板参数指定为DatabaseConnectionDeleter
,构造函数会使用该删除器类的实例来管理对象的销毁。
四、自定义删除器对 std::unique_ptr 性能的影响
- 函数指针删除器
- 使用函数指针作为删除器通常不会带来显著的性能开销。在编译时,编译器可以对函数调用进行优化,尤其是对于简单的删除逻辑。例如,在前面使用
malloc
和free
的示例中,customDelete
函数只是简单地调用free
,编译器可以内联这个函数调用,使得性能接近默认的delete
操作。
- 使用函数指针作为删除器通常不会带来显著的性能开销。在编译时,编译器可以对函数调用进行优化,尤其是对于简单的删除逻辑。例如,在前面使用
- lambda 表达式删除器
- lambda 表达式在编译时会被转换为一个匿名类,其
()
运算符包含了删除逻辑。与函数指针类似,编译器可以对 lambda 表达式中的逻辑进行优化。如果 lambda 表达式的逻辑简单,编译器通常能够有效地内联相关代码,对性能影响较小。例如,在文件句柄的示例中,lambda 表达式中的关闭句柄和释放对象的逻辑相对简单,编译器可以很好地优化它。
- lambda 表达式在编译时会被转换为一个匿名类,其
- 自定义删除器类
- 使用自定义删除器类时,性能也取决于删除器类
()
运算符中的具体逻辑。如果逻辑简单,编译器同样可以进行内联优化。然而,如果()
运算符中包含复杂的逻辑,例如大量的计算或者函数调用,可能会带来一定的性能开销。在数据库连接的示例中,DatabaseConnectionDeleter
的()
运算符逻辑相对简单,性能开销较小。但如果在其中添加复杂的日志记录或者网络通信操作,性能就可能会受到影响。
- 使用自定义删除器类时,性能也取决于删除器类
五、自定义删除器与 std::unique_ptr 的移动语义
- 移动语义的基本原理
std::unique_ptr
支持移动语义,这意味着当一个std::unique_ptr
被移动到另一个std::unique_ptr
时,资源的所有权会转移,而不是进行复制。例如:
std::unique_ptr<int> ptr1(new int(10));
std::unique_ptr<int> ptr2 = std::move(ptr1);
- 在这个例子中,
ptr1
的所有权转移到了ptr2
,ptr1
变为空指针。
- 自定义删除器与移动语义的兼容性
- 无论是使用函数指针、lambda 表达式还是自定义删除器类作为删除器,
std::unique_ptr
的移动语义都能正常工作。 - 以函数指针删除器为例:
- 无论是使用函数指针、lambda 表达式还是自定义删除器类作为删除器,
#include <iostream>
#include <memory>
void customDelete(void* ptr) {
std::cout << "Custom delete function called." << std::endl;
free(ptr);
}
int main() {
std::unique_ptr<char, decltype(&customDelete)> ptr1(static_cast<char*>(malloc(100)), customDelete);
std::unique_ptr<char, decltype(&customDelete)> ptr2 = std::move(ptr1);
return 0;
}
- 在这个代码中,
ptr1
移动到ptr2
时,不仅对象的所有权转移,删除器也会随着移动。同样,对于 lambda 表达式和自定义删除器类作为删除器的情况,移动语义也能正确处理删除器的转移。这确保了在资源所有权转移的过程中,资源的正确释放逻辑仍然有效。
六、自定义删除器在容器中的使用
std::vector
中使用std::unique_ptr
及其自定义删除器- 当我们在
std::vector
中存储std::unique_ptr
时,自定义删除器同样适用。假设我们有一个表示图形对象的基类Shape
,以及它的派生类Circle
和Rectangle
,并且我们希望在std::vector
中存储这些图形对象,并使用自定义删除器:
- 当我们在
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
// 自定义删除器类
class ShapeDeleter {
public:
void operator()(Shape* s) const {
std::cout << "Custom shape deleter called." << std::endl;
delete s;
}
};
int main() {
std::vector<std::unique_ptr<Shape, ShapeDeleter>> shapes;
shapes.emplace_back(new Circle());
shapes.emplace_back(new Rectangle());
for (const auto& shape : shapes) {
shape->draw();
}
return 0;
}
- 在这个例子中,
std::vector
存储了std::unique_ptr<Shape, ShapeDeleter>
,这意味着当std::vector
销毁其元素时,会使用ShapeDeleter
来释放Shape
对象。
std::map
中使用std::unique_ptr
及其自定义删除器- 在
std::map
中也可以类似地使用std::unique_ptr
及其自定义删除器。例如,我们可以将图形对象存储在std::map
中,键是图形的名称:
- 在
#include <iostream>
#include <memory>
#include <map>
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
// 自定义删除器类
class ShapeDeleter {
public:
void operator()(Shape* s) const {
std::cout << "Custom shape deleter called." << std::endl;
delete s;
}
};
int main() {
std::map<std::string, std::unique_ptr<Shape, ShapeDeleter>> shapeMap;
shapeMap["circle"] = std::unique_ptr<Shape, ShapeDeleter>(new Circle());
shapeMap["rectangle"] = std::unique_ptr<Shape, ShapeDeleter>(new Rectangle());
for (const auto& pair : shapeMap) {
std::cout << "Drawing " << pair.first << ": ";
pair.second->draw();
}
return 0;
}
- 在这个例子中,
std::map
的值类型是std::unique_ptr<Shape, ShapeDeleter>
,当std::map
销毁时,会使用ShapeDeleter
来释放Shape
对象。
七、自定义删除器的注意事项
- 删除器类型一致性
- 在使用
std::unique_ptr
时,要确保所有涉及到该std::unique_ptr
的操作都使用相同类型的删除器。例如,如果在创建std::unique_ptr
时使用了函数指针删除器,在后续的移动、赋值等操作中,删除器的类型必须保持一致。否则,可能会导致编译错误或者未定义行为。
- 在使用
- 删除器的可复制性
- 某些情况下,
std::unique_ptr
需要复制删除器,例如在容器中存储std::unique_ptr
时。如果使用函数指针或者 lambda 表达式作为删除器,它们通常是可复制的。然而,对于自定义删除器类,需要确保该类是可复制的,即定义了正确的复制构造函数和赋值运算符。如果删除器类不可复制,在容器中存储std::unique_ptr
时可能会导致编译错误。
- 某些情况下,
- 删除器中的异常处理
- 在自定义删除器中,如果执行删除逻辑时可能抛出异常,要谨慎处理。通常情况下,删除器应该避免抛出异常,因为
std::unique_ptr
的析构函数不会捕获异常。如果删除器抛出异常,可能会导致程序终止或者未定义行为。如果确实需要在删除逻辑中处理异常,可以在删除器内部捕获并处理异常,以确保程序的稳定性。
- 在自定义删除器中,如果执行删除逻辑时可能抛出异常,要谨慎处理。通常情况下,删除器应该避免抛出异常,因为
八、总结自定义删除器的应用场景
- 资源管理多样化
- 当我们使用的资源分配和释放函数不是标准的
new
和delete
时,自定义删除器提供了一种灵活的方式来管理这些资源。如使用malloc
/free
、外部库的资源管理函数等场景。
- 当我们使用的资源分配和释放函数不是标准的
- 复杂清理逻辑
- 对于包含特殊清理需求的对象,例如需要关闭网络连接、释放数据库资源等,自定义删除器可以将这些清理逻辑封装起来,确保对象在销毁时能正确清理所有相关资源。
- 提高代码可维护性和可读性
- 通过使用自定义删除器,将资源释放逻辑与对象的使用逻辑分离,使代码结构更加清晰。例如,在容器中存储
std::unique_ptr
并使用自定义删除器,使得容器的管理和资源的释放逻辑一目了然,提高了代码的可维护性和可读性。
- 通过使用自定义删除器,将资源释放逻辑与对象的使用逻辑分离,使代码结构更加清晰。例如,在容器中存储
通过深入理解和合理使用 std::unique_ptr
的自定义删除器,我们能够更加灵活和高效地管理动态分配的资源,提高 C++ 程序的可靠性和性能。无论是在简单的内存管理场景,还是复杂的资源管理场景中,自定义删除器都为我们提供了强大的工具。