【问题标题】:Throwing exception from constructor memory leak (handle wrapper c++)从构造函数内存泄漏引发异常(句柄包装器 C++)
【发布时间】:2020-06-06 09:23:52
【问题描述】:

我正在尝试编写一个 DriverIterator 类来迭代我通勤中的所有卷。

我知道下一个类可能会导致内存泄漏,因为:

current_ = std::make_unique<Driver>(paths);

可能会抛出异常(出于某种原因..),因此构造将不会完成,因此不会调用析构函数并且不会正确关闭句柄。

据我所知,一旦我收到句柄,我应该停止构建。 但我怎样才能做到这一点? FindFirstVolumeW 还提供了我需要在构造函数完成之前使用的数据。

DriverIterator.hpp:

class DriverIterator final
{
public:
    explicit DriverIterator();
     ~DriverIterator();

private:
    std::unique_ptr<Driver> current_;
    bool is_empty_;
    HANDLE handle_;

public:
    bool is_empty() const;
    Driver get_current() const;
    void next();

private:
    HANDLE start_find();

public:
    DriverIterator(const DriverIterator&) = delete;
    DriverIterator(DriverIterator&&) = delete;
    DriverIterator& operator=(const DriverIterator&) = delete;
    DriverIterator& operator=(DriverIterator&&) = delete;
};

DriverIterator.cpp:

DriverIterator::DriverIterator():
    handle_(start_find())
{}

DriverIterator::~DriverIterator()
{
    try
    {
        FindVolumeClose(handle_);
    }
    catch (...)
    {

    }
}

HANDLE DriverIterator::start_find()
{
    static constexpr uint32_t MAX_BUFFER_SIZE = 1024;
    wchar_t buffer[MAX_BUFFER_SIZE];

    HANDLE handle = FindFirstVolumeW(buffer, MAX_BUFFER_SIZE);
    if (handle == INVALID_HANDLE_VALUE)
    {
        //throw exception
    }

    wchar_t paths[1024];
    DWORD   res_size;

    if (!GetVolumePathNamesForVolumeNameW(buffer, paths, 1024, &res_size))
    {
        //throw exception
    }

    current_ = std::make_unique<Driver>(paths);

    is_empty_ = false;

    return handle;
}

bool DriverIterator::is_empty() const
{
    return is_empty_;
}

Driver DriverIterator::get_current() const
{
    if (is_empty_)
    {
        //throw exception
    }

    return *current_;
}

void DriverIterator::next()
{
    //code...
}

【问题讨论】:

  • 你确定 shared_ptr 是正确的方式吗?由于您不允许复制和分配,因此该指针永远不会共享。我怀疑 unique_ptr 更适合这里
  • 至于您的担心,如果 Driver 类编写正确,其析构函数中的异常不应导致任何不良影响或资源泄漏。由于 Driver 是通过智能指针 make_shared 或 make_unique 构建和维护的,因此这里没有潜在的泄漏,除非 Driver 或 DriverIterator 的用户存在泄漏
  • @MichaelVeksler 你是对的,我应该使用唯一指针而不是共享指针。但是,当我谈到内存泄漏时,我是在谈论无法正确关闭的句柄(从 FindFirstVolumeW 接收),因为 DriverIterator 的构造函数可能会在通过唯一指针构造 Driver 对象时抛出异常。
  • 使用自定义删除器将您的临时资源绑定到unique_ptr
  • 正如@IInspectable 提到的,您可以让带有自定义删除器的 unique_ptr 管理句柄。但就目前而言,我认为您没有任何问题。 Driver对象没有在构造函数中分配,所以构造函数中不会出现异常。但是,最安全的做法是使用 RAII 对象存储任何资源(s.t. 句柄)并在任何相关情况下释放它们。

标签: c++ winapi constructor memory-leaks raii


【解决方案1】:

当构造函数产生异常时,不会调用对象析构函数,而是调用已构造成员的析构函数并释放为对象分配的内存。

因此,惯用的 C++ 方式是为 handle_ 成员定义一个 RAII 包装器,该包装器将提供对底层资源的访问并在析构函数中正确释放它,例如:

template <typename CloseFnT, CloseFnT close_fn>
class UniqueHandle {
public:
    UniqueHandle()
        : handle_(INVALID_HANDLE_VALUE)
    {
    }

    UniqueHandle(HANDLE handle)
        : handle_(handle)
    {
    }

    ~UniqueHandle()
    {
        if (handle_ != INVALID_HANDLE_VALUE) {
            close_fn(handle_);
        }
    }

    UniqueHandle(const UniqueHandle&) = delete;
    UniqueHandle& operator = (const UniqueHandle&) = delete;

    UniqueHandle(UniqueHandle&& other)
        : handle_(INVALID_HANDLE_VALUE)
    {
        std::swap(handle_, other.handle_);
    }

    UniqueHandle& operator = (UniqueHandle&& other)
    {
        std::swap(handle_, other.handle_);
        return *this;
    }

    HANDLE get() const {
        return handle_;
    }

private:
    HANDLE handle_;
};

using UniqueVolumeHandle = UniqueHandle<decltype(&FindVolumeClose), FindVolumeClose>;

那么这个包装器可以在任何地方使用而不是原始 HANDLE:

private:
    UniqueVolumeHandle handle_;
...
    handle_ = FindFirstVolumeW(buffer, MAX_BUFFER_SIZE);
    if (handle_.get() == INVALID_HANDLE_VALUE) {
        ... // handle error, throw exception is OK
    }
...
    // use handle
    if (!FindNextVolumeW(handle.get(), buffer, MAX_BUFFER_SIZE)) {
        ... // handle error
    }

// Return wrapped handle
UniqueVolumeHandle start_find() {
    ...
    UniqueVolumeHandle handle = FindFirstVolumeW(buffer, MAX_BUFFER_SIZE);
    ...
    return handle;
}
...

这种方法例如在microsoft/wil 库中实现。

【讨论】:

  • 如果不实现以下方法会怎样? UniqueHandle& 运算符 = (UniqueHandle&& 其他)
  • @DanielFridman 它用于重新分配值,例如UniqueHandle a = ...; UniqueHandle b = ...; ... ; a = std::move(b); - b 的资源被重新分配给 a 并且 b 值不应在移动后使用(在此示例实现中,最初存储在 a 中的句柄将移动到 b 并在 @ 时释放987654331@被破坏了,但不需要这样,句柄不应该泄漏)。
  • @DanielFridman 那么你不能再分配句柄了。它会严重限制对象的可用性。请注意,不实现方法并不是全部:您必须主动防止对象被复制,通常通过= delete-ing 复制构造函数和赋值。
  • @ReinstateMonica 我明白了,但为什么移动分配的默认 imp 不够?
  • @dewaffled 好的,但你为什么不使用默认的移动分配?
【解决方案2】:

好的,所以我创建了一个名为“FindVolumeHandle”的内部类来包装句柄。

我能以更好的方式做到这一点吗?

DriverIterator.hpp:

class DriverIterator final
{
private:
    class FindVolumeHandle final
    {
    public:
        explicit FindVolumeHandle(const HANDLE handle);
        ~FindVolumeHandle() noexcept;

    private:
        HANDLE handle_;

    public:
        HANDLE get_handle() const;

    public:
        FindVolumeHandle(const FindVolumeHandle&) = delete;
        FindVolumeHandle(FindVolumeHandle&&) = delete;
        FindVolumeHandle& operator=(const FindVolumeHandle&) = delete;
        FindVolumeHandle& operator=(FindVolumeHandle&&) = delete;
    };

public:
    explicit DriverIterator();

private:
    std::unique_ptr<FindVolumeHandle> wrapped_handle_;
    std::unique_ptr<Driver> current_;
    bool is_empty_;

public:
    bool is_empty() const;
    Driver get_current() const;
    void next();

private:
    void initialize();

public:
    DriverIterator(const DriverIterator&) = delete;
    DriverIterator(DriverIterator&&) = delete;
    DriverIterator& operator=(const DriverIterator&) = delete;
    DriverIterator& operator=(DriverIterator&&) = delete;
};

DriverIterator.cpp:

DriverIterator::FindVolumeHandle::FindVolumeHandle(const HANDLE handle) :
    handle_(handle)
{}

DriverIterator::FindVolumeHandle::~FindVolumeHandle() noexcept
{
    try
    {
        FindVolumeClose(handle_);
    }
    catch (...)
    {

    }
}

HANDLE DriverIterator::FindVolumeHandle::get_handle() const
{
    return handle_;
}


DriverIterator::DriverIterator()
{
    initialize();
}

void DriverIterator::initialize()
{
    static constexpr uint32_t MAX_BUFFER_SIZE = 1024;
    wchar_t buffer[MAX_BUFFER_SIZE];

    HANDLE handle = FindFirstVolumeW(buffer, MAX_BUFFER_SIZE);
    if (handle == INVALID_HANDLE_VALUE)
    {
        //throw exception
    }

    wrapped_handle_ = std::make_unique<FindVolumeHandle>(handle);

    wchar_t paths[1024];
    DWORD   res_size;

    if (!GetVolumePathNamesForVolumeNameW(buffer, paths, 1024, &res_size))
    {
        //throw exception
    }

    current_ = std::make_unique<Driver>(paths);

    is_empty_ = false;
}

bool DriverIterator::is_empty() const
{
    return is_empty_;
}

Driver DriverIterator::get_current() const
{
    if (is_empty_)
    {
        //throw exception
    }

    return *current_;
}

void DriverIterator::next()
{
    //code..
}

【讨论】:

  • 你可以将初始化函数合并到构造函数中——额外的间接性似乎没有任何作用
  • 您不需要将包装器存储在unique_ptr 中,只需在包装器本身中定义移动构造函数和移动赋值运算符即可。您也可以使用像 microsoft/wil 这样的辅助通用库来声明这样的包装器,而只需最少的手动工作。
  • @dewaffled 但是我需要在 DriverIterator 构造函数的初始化列表中初始化 Wrapped_handle_,对吗?
  • @DanielFridman 这就是为什么您可以将 INVALID_HANDLE_VALUE 与默认构造函数一起使用,或者使用 std::optional
猜你喜欢
  • 2018-11-18
  • 2019-08-19
  • 1970-01-01
  • 2020-11-02
  • 2015-02-17
  • 2021-06-17
  • 2013-01-12
  • 2013-05-20
  • 1970-01-01
相关资源
最近更新 更多