MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

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 来释放该对象。

二、为什么需要自定义删除器

  1. 非标准内存释放方式
    • 有时候,我们动态分配的资源不是通过 new 分配的,例如使用 malloc 分配的内存,这时不能简单地使用 delete 来释放,而需要调用 free
    • 对于一些外部库提供的资源管理函数,例如 CreateFile 创建的文件句柄,需要使用 CloseHandle 来关闭,而不是 delete
  2. 特殊的清理逻辑
    • 当对象包含一些需要特殊清理的资源时,默认的 delete 操作可能不够。例如,一个对象在创建时打开了一个网络连接,在销毁时不仅要释放对象本身的内存,还需要关闭网络连接。

三、自定义删除器的实现方式

  1. 函数指针作为删除器
    • 我们可以定义一个普通函数作为自定义删除器,然后将该函数指针传递给 std::unique_ptr 的构造函数。
    • 下面是一个使用 mallocfree 管理内存的示例:
#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
  1. 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 表达式。
  1. 自定义删除器类
    • 我们还可以定义一个类,并重载其 () 运算符来实现删除器逻辑。
    • 以下是一个管理数据库连接的示例:
#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 性能的影响

  1. 函数指针删除器
    • 使用函数指针作为删除器通常不会带来显著的性能开销。在编译时,编译器可以对函数调用进行优化,尤其是对于简单的删除逻辑。例如,在前面使用 mallocfree 的示例中,customDelete 函数只是简单地调用 free,编译器可以内联这个函数调用,使得性能接近默认的 delete 操作。
  2. lambda 表达式删除器
    • lambda 表达式在编译时会被转换为一个匿名类,其 () 运算符包含了删除逻辑。与函数指针类似,编译器可以对 lambda 表达式中的逻辑进行优化。如果 lambda 表达式的逻辑简单,编译器通常能够有效地内联相关代码,对性能影响较小。例如,在文件句柄的示例中,lambda 表达式中的关闭句柄和释放对象的逻辑相对简单,编译器可以很好地优化它。
  3. 自定义删除器类
    • 使用自定义删除器类时,性能也取决于删除器类 () 运算符中的具体逻辑。如果逻辑简单,编译器同样可以进行内联优化。然而,如果 () 运算符中包含复杂的逻辑,例如大量的计算或者函数调用,可能会带来一定的性能开销。在数据库连接的示例中,DatabaseConnectionDeleter() 运算符逻辑相对简单,性能开销较小。但如果在其中添加复杂的日志记录或者网络通信操作,性能就可能会受到影响。

五、自定义删除器与 std::unique_ptr 的移动语义

  1. 移动语义的基本原理
    • 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 的所有权转移到了 ptr2ptr1 变为空指针。
  1. 自定义删除器与移动语义的兼容性
    • 无论是使用函数指针、lambda 表达式还是自定义删除器类作为删除器,std::unique_ptr 的移动语义都能正常工作。
    • 以函数指针删除器为例:
#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 表达式和自定义删除器类作为删除器的情况,移动语义也能正确处理删除器的转移。这确保了在资源所有权转移的过程中,资源的正确释放逻辑仍然有效。

六、自定义删除器在容器中的使用

  1. std::vector 中使用 std::unique_ptr 及其自定义删除器
    • 当我们在 std::vector 中存储 std::unique_ptr 时,自定义删除器同样适用。假设我们有一个表示图形对象的基类 Shape,以及它的派生类 CircleRectangle,并且我们希望在 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 对象。
  1. 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 对象。

七、自定义删除器的注意事项

  1. 删除器类型一致性
    • 在使用 std::unique_ptr 时,要确保所有涉及到该 std::unique_ptr 的操作都使用相同类型的删除器。例如,如果在创建 std::unique_ptr 时使用了函数指针删除器,在后续的移动、赋值等操作中,删除器的类型必须保持一致。否则,可能会导致编译错误或者未定义行为。
  2. 删除器的可复制性
    • 某些情况下,std::unique_ptr 需要复制删除器,例如在容器中存储 std::unique_ptr 时。如果使用函数指针或者 lambda 表达式作为删除器,它们通常是可复制的。然而,对于自定义删除器类,需要确保该类是可复制的,即定义了正确的复制构造函数和赋值运算符。如果删除器类不可复制,在容器中存储 std::unique_ptr 时可能会导致编译错误。
  3. 删除器中的异常处理
    • 在自定义删除器中,如果执行删除逻辑时可能抛出异常,要谨慎处理。通常情况下,删除器应该避免抛出异常,因为 std::unique_ptr 的析构函数不会捕获异常。如果删除器抛出异常,可能会导致程序终止或者未定义行为。如果确实需要在删除逻辑中处理异常,可以在删除器内部捕获并处理异常,以确保程序的稳定性。

八、总结自定义删除器的应用场景

  1. 资源管理多样化
    • 当我们使用的资源分配和释放函数不是标准的 newdelete 时,自定义删除器提供了一种灵活的方式来管理这些资源。如使用 malloc/free、外部库的资源管理函数等场景。
  2. 复杂清理逻辑
    • 对于包含特殊清理需求的对象,例如需要关闭网络连接、释放数据库资源等,自定义删除器可以将这些清理逻辑封装起来,确保对象在销毁时能正确清理所有相关资源。
  3. 提高代码可维护性和可读性
    • 通过使用自定义删除器,将资源释放逻辑与对象的使用逻辑分离,使代码结构更加清晰。例如,在容器中存储 std::unique_ptr 并使用自定义删除器,使得容器的管理和资源的释放逻辑一目了然,提高了代码的可维护性和可读性。

通过深入理解和合理使用 std::unique_ptr 的自定义删除器,我们能够更加灵活和高效地管理动态分配的资源,提高 C++ 程序的可靠性和性能。无论是在简单的内存管理场景,还是复杂的资源管理场景中,自定义删除器都为我们提供了强大的工具。