有时,即使在编写具有 RAII 优点的现代 C++ 时,您也需要手动管理资源。我想知道我通常使用的技术是否有名称。
资源管理很乏味,但也许你正在调用一个旧的图书馆或其他什么。现在我假设有一个函数
acquire()
需要在某些函数的开头调用,如果这样做,你必须确保very在函数退出时,对应的release()
被称为。
手动执行此操作意味着必须确保 (a) 函数中每个
release()
之前都有一个 return
(或 void
函数的函数结尾),(b) 没有例外可以在不发生任何情况下转义该函数release()
被调用,并且 (c) 该函数的每个未来维护者都知道并维护这一点。这已经是一场灾难了。 :)
相反,我喜欢做的是创建一个“终结器对象”,我称之为
acquire()
。为此,我创建了一个 Finally
类模板,大致如下:
template<class Func>
class Finally {
public:
Finally(Func f) : storedFunc(f) {}
~Finally() { storedFunc(); }
private:
Func storedFunc;
};
(暂时忽略异常处理、有效地将函数移至
storedFunc
等)
然后使用如下:
void someFunction() {
…
acquire();
Finally finalizer{[](){ release(); }};
…
}
重要的是这两条线紧接着彼此。这样,当
someFunction
结束时(无论为什么......),本地 finalizer
对象将被销毁,并在存储的 lambda 中调用 release()
。
这项技术有既定的名称吗?
我还没有真正看到这个在很多地方被使用。我不知道这是为什么。我当然知道这种技术的一些缺点:
catch(...)
中出现 ~Finally()
(带有一些亮红色的错误日志记录),具体取决于“程序崩溃”或“程序泄漏资源”是否被认为更糟糕。Finally()
的构造函数抛出异常,则资源被泄漏。我们只是确保它不会。 :)-O1
开始完全优化(参见 at Godbolt)是否还有其他我遗漏的重大陷阱,导致这是一个坏主意?
您描述的技术通常称为“资源获取即初始化(RAII)”。 RAII 是一种 C++ 编程习惯,其中资源的生命周期与对象的范围相关。当对象(在本例中为 Final 对象)超出范围时,会自动调用其析构函数,从而允许您释放获取的资源。 在您的示例中,
Finally类中的 lambda 函数充当清理操作(释放资源),并在Finally对象在作用域末尾被销毁时自动调用。 以下是一些注意事项和潜在的陷阱:
异常安全:如果Finally内部的lambda在执行过程中抛出异常,可能会导致意外的行为,特别是在需要执行其他清理操作的情况下。正如您提到的,添加 catch 块来记录错误是一个很好的做法。
初始化顺序:请谨慎对待具有此模式的对象的构造和销毁顺序。如果您有多个具有清理操作的对象,那么它们的销毁顺序很重要。
资源泄漏的可能性:正如你提到的,如果Finally的构造函数抛出异常,则资源可能会泄漏。确保构造函数不抛出异常对于此模式的可靠性至关重要。
性能:虽然性能开销通常很小并且可以通过编译器进行优化,但仍然需要注意,特别是在性能关键型代码中。 总体而言,RAII 是 C++ 中用于资源管理的强大且广泛使用的习惯用法,并且您对它的使用与 RAII 的原则非常一致。请注意特定用例中的异常安全和对象生命周期注意事项。