我读过一个随机的网上报价约“RAII在C ++中是唯一例外可能” 一旦太多。我不能再忍受了。小编c++学习群825414254获取c++一整套系统性的学习资料还有数十套pdf
TL; DR:这篇文章不是关于例外是好还是坏。它的 作用是RAII作为一种C ++动态资源管理技术,它独立存在,无论是否有例外都是有用的。特别是,我想解释为什么即使你的C ++代码中禁用了异常,RAII确实很有用。
基础 让我们看看RAII的海报孩子,一个自动关闭句柄来包装文件* [1]:
class FileHandle { public: FileHandle(const char* name, const char* mode) { f_ = fopen(name, mode); } FILE* file() { return f_; } ~FileHandle() { if (f_ != nullptr) { fclose(f_); } } private: FILE* f_; };
以下是我们如何使用它的示例: 的
std::string do_stuff_with_file(std::string filename) { FileHandle handle(filename.c_str(), "r"); int firstchar = fgetc(handle.file()); if (firstchar != '$') { return "bad bad bad"; } return std::string(1, firstchar); }
请记住:这里没有例外 - 代码是使用-fno-exceptions构建的,并且没有try语句。但是,FileHandle的RAII-ness 仍然很重要,因为do_stuff_with_file有两个退出点,并且每个文件都必须关闭。do_stuff_with_file是一个简短的函数。在具有多个出口点的较大功能中,管理资源释放变得更容易出错,并且RAII技术是最重要的。 RAII的本质是在堆栈分配对象的构造函数中获取一些资源,并在析构函数中释放它。编译器保证当这些对象超出范围时,将按正确的顺序调用所有堆栈分配的对象的析构函数,无论是由于引发的异常还是仅仅因为函数返回。 RAII并不意味着你必须在构造函数中分配或实际创建任何东西。它可以执行任何具有必须稍后执行的逻辑“撤消”的操作。一个很好的例子是引用计数。许多数据库和类似的软件库具有提供数据访问的“游标”抽象。以下是我们在使用它时如何安全地增加和减少给定游标的引用计数:
class CursorGuard { public: CursorGuard(Cursor* cursor) : cursor_(cursor) { cursor_->incref(); } Cursor* cursor() { return cursor_; } ~CursorGuard() { cursor_->decref(); } private: Cursor* cursor_; }; void work_with_cursor(Cursor* cursor) { CursorGuard cursor_guard(cursor); if (cursor_guard.cursor()->do_stuff()) { // ... do something return; } // ... do something else return; }
再次,在这里使用RAII确保在任何情况下 work_with_cursor都不会泄漏游标引用:一旦递增,无论函数如何最终返回,都保证是递减的。
RAII在标准库中 即使在标准库中,这种“防护”RAII类也非常有用且广泛。C ++ 11线程库具有用于互斥锁的lock_guard,例如:
void safe_data_munge(std::mutex& shared_mutex, Data* shared_data) { std::lock_guard<std::mutex> lock(shared_mutex); shared_data->munge(); if (...) { shared_data(); return; } shared_data->munge_less(); return; }
std :: lock_guard在其构造函数中锁定互斥锁,并在其析构函数中解锁它,确保在整个 safe_data_munge中对共享数据的访问受到保护, 并且实际解锁始终发生。
RAII和C ++ 11 关于标准库的主题,我不能不提及它们中最重要的RAII对象--std :: unique_ptr。C和C ++中的资源管理是一个庞大而复杂的主题; 在C ++代码中管理的最常见的资源是堆内存。在C ++ 11之前,有许多针对“智能指针”的第三方解决方案,而C ++ 11的移动语义 最终允许该语言为RAII提供非常强大的智能指针:
void using_big_data() { std::unique_ptr<VeryVeryBigData> data(new VeryVeryBigData); data->do_stuff(); if (data->do_other_stuff(42)) { return; } data->do_stuff(); return; }
无论我们如何处理数据,无论函数返回何处,分配的内存都将被释放。如果您的编译器支持C ++ 14,那么使用std :: make_unique可以使创建指针的行更简洁:
// Good usage of 'auto': removes the need to repeat a (potentially long) // type name, and the actual type assigned to 'data' is trivially obvious. auto data = std::make_unique<VeryVeryBigData>();
std :: unique_ptr是多功能的,有其他用途,虽然在这里我只关注它作为堆内存的RAII启动器的价值。 为了强调C ++ 11对于正确RAII的重要性:在C ++ 11之前,没有移动语义,我们可以编写的唯一“智能”指针实际上有点愚蠢,因为它们导致了太多的复制和开销。根本没有办法将对象从一个函数“转移”到另一个函数,而没有相当大的开销。由于C ++程序员通常都是从代码中挤出最后一点性能,所以很多人宁愿生活在边缘并处理原始指针。使用C ++ 11和std :: unique_ptr,可以有效地移动并且不占用额外的内存,这个问题要严重得多,安全性也不一定要以性能为代价。
RAII用其他语言 关于C ++的一个常见问题是“为什么C ++没有 像Java,C#和Python等其他语言享受的终结构?”。Stroustrup自己给出的答案 是RAII是替代品。Stroustrup的原因(正确地说,恕我直言),在现实的代码库中,资源的获取和发布远远多于不同的“种类”资源,因此RAII导致代码更少。此外,由于您对RAII包装器进行一次编码而不必记住手动释放资源,因此它不易出错。以下是使用假设的finally结构重写的上面的work_with_cursor示例:
// Warning: this is not real C++ void work_with_cursor(Cursor* cursor) { try { cursor->incref(); if (cursor->do_stuff()) { // ... do something return; } // ... do something else return; } finally { cursor->decref(); } }
是的,这是更多的代码。但更大的问题是记住调用 cursor-decref()。由于大型代码库一直在处理资源,实际上你最终会尝试...最终阻塞每个函数的主体并且必须记住要释放的资源。使用我们的CursorGuard 助手,所有这些都以保护类本身的一次性定义为代价。 这里提到的一个很好的例子是Python。尽管Python有一个finally 结构,但在现代Python代码中,使用语句的替代方法得到了更广泛的应用。与支持“上下文管理器”,这是非常类似于C ++ RAII。与报表最终会被更灵活和漂亮,而不是使用最后,这就是为什么你会看到更多他们的惯用代码。
那么例外呢? 到目前为止,我希望这篇文章使您确信即使禁用了异常,C ++中的RAII技术也很重要且有用。然而,人们在RAII和例外之间存在密切关联,因为编写没有RAII的异常安全代码几乎是不可能的。启用异常后,我们不仅需要检查函数中的每个显式return语句,以确定可以泄漏资源的位置。每一行都成了嫌疑人。功能或方法调用?可以投掷。在堆栈上创建一个新的非POD对象?可以投掷。将一个对象复制到另一个对象?是的,可以扔。a + b?可以投入+运算符。 异常和RAII之间的另一个强有力的联系是构造函数。构造函数不能具有返回值。因此,如果构造函数遇到错误条件,则要么抛出异常,要么标记某个内部错误状态。后者有其问题(这就是为什么在代码中建议使用替代的构造方法而没有例外),因此抛出异常是最常见的方法。由于RAII对异常非常重要,并且因为RAII和构造函数齐头并进(请记住 - RAII在构造对象时启动),这个链接深深地融入了C ++学生的思维中。 但RAII不仅仅是例外。它是关于C ++中规范的资源管理。因此,假设RAII以某种方式表示您的代码是一个异常混乱的混乱是没有意义的。甚至它根本就使用例外。攻击C ++是因为它的异常安全问题是合法的,但攻击RAII则不那么重要,因为RAII只是一个解决方案,它不是问题的根源。 最后,从更个人的角度来说,我将补充一点,虽然我不是C ++异常的忠实粉丝,但我是 RAII的忠实粉丝。当我今天编写C ++代码时,我宁愿根本不使用异常,或者至少将它们限制并限制在程序中的小区域。但是我一直使用RAII,无论是在std :: unique_ptr等标准库类中,还是在我自己的代码中。在我看来,它是C ++最好和最有用的功能之一,可以帮助保持大型代码库的安全和安全。