C ++是否支持“终于”阻止? (我听说过这个“RAII”是什么?)

C ++是否支持“ 终于 ”阻止?

什么是RAII成语

C ++的RAII成语和C#的'using'语句有什么区别?

不,C ++不支持“最终”块。 原因是C ++支持RAII:“资源获取是初始化” – 一个真正有用的概念,一个糟糕的名字

这个想法是一个对象的析构函数负责释放资源。 当对象具有自动存储持续时间时,对象的析构函数将在创build块时退出 – 即使该块在出现exception时退出。 这是Bjarne Stroustrup对这个话题的解释

RAII的一个常见用法是locking一个互斥锁:

// A class with implements RAII class lock { mutex &m_; public: lock(mutex &m) : m_(m) { m.acquire(); } ~lock() { m_.release(); } }; // A class which uses 'mutex' and 'lock' objects class foo { mutex mutex_; // mutex for locking 'foo' object public: void bar() { lock scopeLock(mutex_); // lock object. foobar(); // an operation which may throw an exception // scopeLock will be destructed even if an exception // occurs, which will release the mutex and allow // other functions to lock the object and run. } }; 

RAII还简化了使用对象作为其他类的成员。 当拥有的类被破坏时,RAII类pipe理的资源被释放,因为RAII被pipe理类的析构函数被调用。 这意味着,当您在一个pipe理资源的类中使用RAII时,可以使用一个非常简单的,甚至是所有者类的默认析构函数,因为它不需要手动pipe理它的成员资源生命周期。 (感谢Mike B指出了这一点。)

对于那些熟悉C#或VB.NET的家庭,您可能会认识到,RAII类似于使用IDisposable和'using'语句的.NET确定性破坏 。 的确,这两种方法非常相似。 主要区别在于RAII将确定性地释放任何types的资源(包括内存)。 在.NET中实现IDisposable(甚至.NET语言的C ++ / CLI)时,资源将被确定性地释放,除了内存。 在.NET中,内存不是确定性地释放; 内存只在垃圾收集周期中释放。

†有些人认为“销毁就是资源的放弃”是RAII习语的更准确的名称。

在C ++中,由于RAII,最后不需要。

RAII将exception安全的责任从对象的用户移动到对象的devise者(和执行者)。 我认为这是正确的地方,因此只需要在devise/实现过程中获得一次正确的exception安全。 通过最终使用,每次使用对象时都需要获得exception安全性。

另外海事组织的代码看起来整洁(见下文)。

例:

一个数据库对象。 为了确保使用数据库连接,必须打开和closures数据库连接。 通过使用RAII,这可以在构造函数/析构函数中完成。

C ++就像RAII一样

 void someFunc() { DB db("DBDesciptionString"); // Use the db object. } // db goes out of scope and destructor closes the connection. // This happens even in the presence of exceptions. 

RAII的使用使正确使用DB对象非常容易。 无论我们如何尝试和滥用,DB对象都将通过使用析构函数正确closures自身。

Java最后一样

 void someFunc() { DB db = new DB("DBDesciptionString"); try { // Use the db object. } finally { // Can not rely on finaliser. // So we must explicitly close the connection. try { db.close(); } catch(Throwable e) { /* Ignore */ // Make sure not to throw exception if one is already propagating. } } } 

最终使用时,对象的正确使用被委托给对象的用户。 对象用户有责任正确地closures数据库连接。 现在你可以争辩说,这可以在finaliser中完成,但是资源的可用性或其他限制可能有限,因此你通常希望控制对象的释放而不依赖于垃圾收集器的非确定性行为。

这也是一个简单的例子。
当你有多个需要发布的资源时,代码会变得复杂。

更详细的分析可以在这里find: http : //accu.org/index.php/journals/236

在C ++ 11中,如果需要的话,RAII允许做一个最后的:

 namespace detail { //adapt to your "private" namespace template <typename F> struct FinalAction { FinalAction(F f) : clean_{f} {} ~FinalAction() { if(enabled_) clean_(); } void disable() { enabled_ = false; }; private: F clean_; bool enabled_{true}; }; } template <typename F> detail::FinalAction<F> finally(F f) { return detail::FinalAction<F>(f); } 

使用示例:

 #include <iostream> int main() { int* a = new int; auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!\n"; }); std::cout << "doing something ...\n"; } 

输出将是:

 doing something... leaving the block, deleting a! 

我个人使用这几次来确保在C ++程序中closuresPOSIX文件描述符。

有一个真正的类pipe理资源,所以避免任何forms的泄漏通常是更好的,但最终这是有用的,在一个类,听起来像一个矫枉过正的情况下。

另外,我最终还是比其他语言更好,因为如果自然使用的话,你可以在开放代码附近写下结束码(在我的例子中新build删除 ),并且在C ++中像往常一样按照LIFO命令的结构进行破坏。 唯一的缺点是,你得到一个你真的不使用的自动variables,lambda语法使它有点吵(在我的例子中,第四行只有最后一个词和右边的{}块是有意义的,rest本质上是噪音)。

另一个例子:

  [...] auto precision = std::cout.precision(); auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } ); std::cout << std::setprecision(3); 

禁用成员是有用的,如果最后只有在失败的情况下被调用。 例如,您必须在三个不同的容器中复制一个对象,您可以设置终止以撤消每个副本,并在所有副本都成功后禁用。 这样做,如果破坏不能抛出,就保证了有力的保证。

禁用示例:

 //strong guarantee void copy_to_all(BIGobj const& a) { first_.push_back(a); auto undo_first_push = finally([first_&] { first_.pop_back(); }); second_.push_back(a); auto undo_second_push = finally([second_&] { second_.pop_back(); }); third_.push_back(a); //no necessary, put just to make easier to add containers in the future auto undo_third_push = finally([third_&] { third_.pop_back(); }); undo_first_push.disable(); undo_second_push.disable(); undo_third_push.disable(); } 

除了使用基于堆栈的对象简化清理之外,RAII也很有用,因为当对象是另一个类的成员时,会发生相同的“自动”清理。 当拥有的类被破坏时,由RAII类pipe理的资源被清理,因为该类的dtor被调用。

这意味着,当你到达RAII必杀技和所有class级成员使用RAII(如智能指针),你可以逃脱一个非常简单的(甚至可能是默认的)所有者类的dtor,因为它不需要手动pipe理它成员资源的生命周期。

为什么即使托pipe语言提供了一个finally块,尽pipe资源被垃圾回收器自动释放了?

实际上,基于垃圾收集器的语言需要“最终”更多。 垃圾收集器不会及时销毁您的对象,因此无法正确清理与内存不相关的问题。

就dynamic分配的数据而言,很多人会认为你应该使用智能指针。

然而…

RAII将exception安全的责任从对象的用户移交给devise者

可悲的是,这是它自己的倒台。 老C编程习惯难改。 当你使用C或C风格编写的库时,RAII将不会被使用。 没有重写整个API前端,这就是你必须使用的。 然后缺乏“终于”真的咬人。

对不起挖掘这样一个旧线程,但在以下推理中有一个重大错误:

RAII将exception安全的责任从对象的使用者转移到对象的devise者(和执行者)。 我认为这是正确的地方,因此只需要在devise/实现过程中获得一次正确的exception安全。 通过最终使用,每次使用对象时都需要获得exception安全性。

通常情况下,你必须处理dynamic分配的对象,对象的dynamic数量等。在try-block中,一些代码可能会创build许多对象(在运行时确定多less个对象),并将指针存储在列表中。 现在,这不是一个奇特的场景,但非常普遍。 在这种情况下,你会想写一些东西

 void DoStuff(vector<string> input) { list<Foo*> myList; try { for (int i = 0; i < input.size(); ++i) { Foo* tmp = new Foo(input[i]); if (!tmp) throw; myList.push_back(tmp); } DoSomeStuff(myList); } finally { while (!myList.empty()) { delete myList.back(); myList.pop_back(); } } } 

当然,列表本身在超出范围时会被销毁,但是这不会清理已经创build的临时对象。

相反,你必须走上丑陋的路线:

 void DoStuff(vector<string> input) { list<Foo*> myList; try { for (int i = 0; i < input.size(); ++i) { Foo* tmp = new Foo(input[i]); if (!tmp) throw; myList.push_back(tmp); } DoSomeStuff(myList); } catch(...) { } while (!myList.empty()) { delete myList.back(); myList.pop_back(); } } 

另外:为什么即使托pipe语言提供了一个finally块,尽pipe资源被垃圾回收器自动释放了?

提示:还有更多的事情可以用“终于”来完成,而不仅仅是内存释放。

FWIW,微软的Visual C ++确实支持try,最后它在MFC应用程序中被用作捕获严重exception的方法,否则会导致崩溃。 例如;

 int CMyApp::Run() { __try { int i = CWinApp::Run(); m_Exitok = MAGIC_EXIT_NO; return i; } __finally { if (m_Exitok != MAGIC_EXIT_NO) FaultHandler(); } } 

我过去曾经使用过这个function来做一些事情,比如在退出之前保存打开文件的备份。 某些JITdebugging设置将会破坏这个机制。

不是,但是你可以在某种程度上效仿它们,例如:

 int * array = new int[10000000]; try { // Some code that can throw exceptions // ... throw std::exception(); // ... } catch (...) { // The finally-block (if an exception is thrown) delete[] array; // re-throw the exception. throw; } // The finally-block (if no exception was thrown) delete[] array; 

请注意,在重新引发原始exception之前,finally块本身可能会抛出exception,从而丢弃原始exception。 这与Java finally块中的行为完全相同。 另外,你不能在try&catch块中使用return

我想出了一个可以 Java中的finally关键字一样使用的finallymacros。 它使用std::exception_ptr和朋友,lambda函数和std::promise ,所以它需要C++11或更高版本; 还使用了clang支持的复合语句expression式 GCC扩展。

警告 :此答案的早期版本使用了另一个概念的实现,但有更多的限制。

首先,我们来定义一个辅助类。

 #include <future> template <typename Fun> class FinallyHelper { template <typename T> struct TypeWrapper {}; using Return = typename std::result_of<Fun()>::type; public: FinallyHelper(Fun body) { try { execute(TypeWrapper<Return>(), body); } catch(...) { m_promise.set_exception(std::current_exception()); } } Return get() { return m_promise.get_future().get(); } private: template <typename T> void execute(T, Fun body) { m_promise.set_value(body()); } void execute(TypeWrapper<void>, Fun body) { body(); } std::promise<Return> m_promise; }; template <typename Fun> FinallyHelper<Fun> make_finally_helper(Fun body) { return FinallyHelper<Fun>(body); } 

然后是实际的macros。

 #define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try #define finally }); \ true; \ ({return __finally_helper.get();})) \ /***/ 

它可以像这样使用:

 void test() { try_with_finally { raise_exception(); } catch(const my_exception1&) { /*...*/ } catch(const my_exception2&) { /*...*/ } finally { clean_it_all_up(); } } 

std::promise的使用使其非常容易实现,但是也可能引入了相当多的不必要的开销,可以通过从std::promise重新实现所需的function来避免。


CAVEAT:有几件事情不像finally的Java版本那样工作。 closures我的头顶上:

  1. trycatch()的块中使用break语句不能从外部循环中break ,因为它们存在于lambda函数中;
  2. try之后必须至less有一个catch()块:这是C ++的要求;
  3. 如果该函数的返回值不是void而是在trycatch()'s块内没有返回值,那么编译将会失败,因为finallymacros将扩展到将要返回void代码。 这可能是错误的,因为有一个finally_noreturnmacros。

总而言之,我不知道自己是否会使用这些东西,但玩起来很有趣。 🙂

正如很多人所说,解决scheme是使用C ++ 11function来避免最终块。 其中一个特点是unique_ptr

这是Mephane用RAII模式写的答案。

 #include <vector> #include <memory> #include <list> using namespace std; class Foo { ... }; void DoStuff(vector<string> input) { list<unique_ptr<Foo> > myList; for (int i = 0; i < input.size(); ++i) { myList.push_back(unique_ptr<Foo>(new Foo(input[i]))); } DoSomeStuff(myList); } 

在这里介绍一些关于在C ++标准库容器中使用unique_ptr的介绍

我想提供一个替代scheme。

如果你最后要阻塞总是被调用的,就把它放在最后一个catch块之后(这可能是catch( ... )捕获不知名的exception)

 try{ // something that might throw exception } catch( ... ){ // what to do with uknown exception } //final code to be called always, //don't forget that it might throw some exception too doSomeCleanUp(); 

如果你最后想要阻塞作为最后一件事情时,抛出任何exception,你可以使用布尔局部variables – 在运行之前,你把它设置为false,并把真正的赋值在try块的最后,然后在catch块检查variables值:

 bool generalAppState = false; try{ // something that might throw exception //the very end of try block: generalAppState = true; } catch( ... ){ // what to do with uknown exception } //final code to be called only when exception was thrown, //don't forget that it might throw some exception too if( !generalAppState ){ doSomeCleanUpOfDirtyEnd(); } //final code to be called only when no exception is thrown //don't forget that it might throw some exception too else{ cleanEnd(); } 

我有一个用例,我认为finally 应该成为C ++ 11语言的一个完全可以接受的部分,因为我认为从stream的angular度来看更容易阅读。 我的用例是消费者/生产者线程链,在运行结束时发送sentinel nullptr来closures所有线程。

如果C ++支持它,你会希望你的代码看起来像这样:

  extern Queue downstream, upstream; int Example() { try { while(!ExitRequested()) { X* x = upstream.pop(); if (!x) break; x->doSomething(); downstream.push(x); } } finally { downstream.push(nullptr); } } 

我认为这是更合乎逻辑的,把你最后的声明在循环的开始,因为它发生在循环已经退出之后…但这是一厢情愿的想法,因为我们不能用C ++来做到这一点。 请注意, downstream队列连接到另一个线程,所以不能在downstream的析构函数中放置标记push(nullptr) ,因为此时它不能被销毁……它需要保持活动状态直到另一个线程收到nullptr

所以这里是如何使用lambda的RAII类来做同样的事情:

  class Finally { public: Finally(std::function<void(void)> callback) : callback_(callback) { } ~Finally() { callback_(); } std::function<void(void)> callback_; }; 

这里是你如何使用它:

  extern Queue downstream, upstream; int Example() { Finally atEnd([](){ downstream.push(nullptr); }); while(!ExitRequested()) { X* x = upstream.pop(); if (!x) break; x->doSomething(); downstream.push(x); } } 
 try { ... goto finally; } catch(...) { ... goto finally; } finally: { ... }