C ++是否支持'finally'块? (我听到的'RAII'是什么?)

时间:2008-10-02 07:14:01

标签: c++ exception raii finally c++-faq

C ++是否支持“finally”阻止?

什么是RAII idiom

C ++的RAII习语与C#'s 'using' statement之间有什么区别?

16 个答案:

答案 0 :(得分:252)

不,C ++不支持'finally'块。原因是C ++反而支持RAII:“资源获取是初始化” - 一个可怜的名字 ,这是一个非常有用的概念。

这个想法是一个对象的析构函数负责释放资源。当对象具有自动存储持续时间时,将在创建对象的块退出时调用该对象的析构函数 - 即使在存在异常时退出该块也是如此。以下是该主题的Bjarne Stroustrup's explanation

RAII的常见用途是锁定互斥锁:

// 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类管理的资源被释放,因为RAII管理的类的析构函数被调用。这意味着当您对管理资源的类中的所有成员使用RAII时,您可以使用非常简单的,甚至是默认的所有者类的析构函数,因为它不需要手动管理其成员资源生命周期。 (感谢 Mike B 指出这一点。)

对于那些使用C#或VB.NET的人,您可能会发现RAII与.NET deterministic destruction using IDisposable and 'using' statements类似。的确,这两种方法非常相似。主要区别在于RAII将确定性地释放任何类型的资源 - 包括内存。在.NET中实现IDisposable(甚至是.NET语言C ++ / CLI)时,除了内存之外,资源将被确定性地释放。在.NET中,内存不是确定性释放的;内存仅在垃圾收集周期中释放。

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

答案 1 :(得分:74)

在C ++中,由于RAII,最终需要 NOT

RAII将异常安全的责任从对象的用户转移到对象的设计者(和实现者)。我认为这是正确的地方因为你只需要一次异常安全(在设计/实现中)。通过最后使用,每次使用对象时都需要使异常安全性正确。

此外,IMO代码看起来更整洁(见下文)。

示例:

数据库对象。要确保使用DB连接,必须打开和关闭它。通过使用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对象都会通过使用析构函数正确关闭它。

Java Like Finally

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.
        }
    }
}

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

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

可以在此处找到更详细的分析:http://accu.org/index.php/journals/236

答案 2 :(得分:54)

RAII通常更好,但您可以很容易地在C ++中使用 finally 语义。使用少量代码。

此外,C ++ Core Guidelines give finally.

以下是指向GSL Microsoft implementation的链接以及指向Martin Moene implementation

的链接

Bjarne Stroustrup多次表示,GSL中的所有内容最终都符合标准。因此,它应该是一种面向未来的方式,使用最终

如果你愿意,你可以轻松实现自己,继续阅读。

在C ++ 11中,RAII和lambdas允许最终形成一个通用:

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 ++程序中关闭POSIX文件描述符。

拥有一个真正的类来管理资源并因此避免任何类型的泄漏通常会更好,但是这个最终在使类听起来像过度杀伤的情况下非常有用。

此外,我比其他语言最终更喜欢它,因为如果自然使用,您可以在开头代码附近写一个结束代码(在我的示例中 new 删除)和销毁遵循LIFO顺序构造,像往常一样在C ++中。唯一的缺点是你得到一个你真正使用的自动变量而lambda语法使它有点吵(在我的例子中,第四行只有最后和{} - 右侧的块是有意义的,其余的基本上是噪音)。

另一个例子:

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

如果仅在发生故障时必须调用 finally ,则禁用成员非常有用。例如,您必须在三个不同的容器中复制对象,您可以设置 finally 以撤消每个副本,并在所有副本成功后禁用。这样做,如果破坏不能抛出,则确保强有力的保证。

停用示例:

//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(); }

如果你不能使用C ++ 11,你仍然可以终于,但代码变得更加冗长。只需定义一个只包含构造函数和析构函数的结构,构造函数就会引用所需的任何内容,析构函数会执行您需要的操作。这基本上就是lambda所做的,手动完成。

#include <iostream>
int main() {
    int* a = new int;

    struct Delete_a_t {
        Delete_a_t(int* p) : p_(p) {}
       ~Delete_a_t() { delete p_; std::cout << "leaving the block, deleting a!\n"; }
        int* p_;
    } delete_a(a);

    std::cout << "doing something ...\n"; }

答案 3 :(得分:30)

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

这意味着当你到达RAII天堂并且一个班级中的所有成员都使用RAII(如智能指针)时,你可以为所有者类逃脱一个非常简单(甚至是默认)的dtor,因为它不需要手动管理其成员资源生命周期。

答案 4 :(得分:30)

  

为什么即使托管语言提供了一个finally块,尽管垃圾收集器会自动解除分配资源?

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

就动态分配数据而言,许多人认为你应该使用智能指针。

...然而

  

RAII将异常安全的责任从对象的用户转移到设计者

可悲的是,这是它自己的垮台。旧的C编程习惯很难。当您使用以C或非常C风格编写的库时,将不会使用RAII。没有重写整个API前端,这正是你必须要处理的。 然后缺乏“终于”真的咬人。

答案 5 :(得分:7)

很抱歉挖掘这样一个旧线程,但是在以下推理中存在重大错误:

  

RAII将异常安全的责任从对象的用户转移到对象的设计者(和实现者)。我认为这是正确的地方因为你只需要一次异常安全(在设计/实现中)。通过最后使用,每次使用对象时都需要使异常安全性正确。

通常情况下,您必须处理动态分配的对象,动态的对象数等。在try-block中,某些代码可能会创建许多对象(在运行时确定多少个对象)并将指针存储在一个对象中。名单。现在,这不是一个奇特的场景,但很常见。在这种情况下,你想写一些像

这样的东西
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();
    }
  }
}

当然,当超出范围时,列表本身将被销毁,但这不会清除您创建的临时对象。

相反,你必须走丑路:

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();
  }
}

另外:为什么即使管理的语言提供了最终阻止,尽管垃圾收集器会自动解除分配资源?

提示:你可以用“终于”做更多事情,而不仅仅是内存释放。

答案 6 :(得分:7)

另一个&#34;终于&#34;使用C ++ 11 lambda函数阻止仿真

template <typename TCode, typename TFinallyCode>
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
    try
    {
        code();
    }
    catch (...)
    {
        try
        {
            finally_code();
        }
        catch (...) // Maybe stupid check that finally_code mustn't throw.
        {
            std::terminate();
        }
        throw;
    }
    finally_code();
}

我们希望编译器能够优化上面的代码。

现在我们可以编写如下代码:

with_finally(
    [&]()
    {
        try
        {
            // Doing some stuff that may throw an exception
        }
        catch (const exception1 &)
        {
            // Handling first class of exceptions
        }
        catch (const exception2 &)
        {
            // Handling another class of exceptions
        }
        // Some classes of exceptions can be still unhandled
    },
    [&]() // finally
    {
        // This code will be executed in all three cases:
        //   1) exception was not thrown at all
        //   2) exception was handled by one of the "catch" blocks above
        //   3) exception was not handled by any of the "catch" block above
    }
);

如果你希望你可以将这个成语包装成&#34;尝试 - 终于&#34;宏:

// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};

#define begin_try    with_finally([&](){ try
#define finally      catch(never_thrown_exception){throw;} },[&]()
#define end_try      ) // sorry for "pascalish" style :(

现在&#34;终于&#34;块在C ++ 11中可用:

begin_try
{
    // A code that may throw
}
catch (const some_exception &)
{
    // Handling some exceptions
}
finally
{
    // A code that is always executed
}
end_try; // Sorry again for this ugly thing

就我个人而言,我不喜欢&#34;宏观&#34;版本&#34;终于&#34;成语,并希望使用纯&#34; with_finally&#34;即使在这种情况下语法更笨重,也会起作用。

您可以在此处测试上面的代码:http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

<强> PS

如果您的代码中需要 finally 块,那么scoped guardsON_FINALLY/ON_EXCEPTION宏可能更适合您的需求。

以下是使用ON_FINALLY / ON_EXCEPTION:

的简短示例
void function(std::vector<const char*> &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    ...

答案 7 :(得分:5)

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

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

我过去曾经用过这个来做退出之前保存打开文件备份的事情。某些JIT调试设置会打破这种机制。

答案 8 :(得分:5)

如其他答案所指出,C ++可以支持类似finally的功能。该功能的实现可能最接近标准语言的一部分,是C++ Core Guidelines附带的实现,implementation of finally是由Bjarne Stoustrup和Herb Sutter编辑的一组使用C ++的最佳实践。 Guidelines Support LibraryUse a final_action object to express cleanup if no suitable resource handle is available(GSL)的一部分。在整个准则中,建议在处理旧式界面时使用finally,它也有自己的准则,标题为Paolo.Bolzoni's answer

因此,C ++不仅仅支持finally,而且实际上建议在许多常见用例中使用它。

使用GSL实现的示例如下:

#include <gsl/gsl_util.h>

void example()
{
    int handle = get_some_resource();
    auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });

    // Do a lot of stuff, return early and throw exceptions.
    // clean_that_resource will always get called.
}

GSL的实现和用法与{{3}}中的非常相似。一个区别是gsl::finally()创建的对象缺少disable()调用。如果您需要该功能(例如,在组装好资源后返回资源,并且肯定不会发生任何异常),那么您可能更喜欢Paolo的实现。否则,使用GSL与您将获得的标准化功能非常接近。

答案 9 :(得分:3)

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

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;

请注意,finally块可能会在重新抛出原始异常之前抛出异常,从而丢弃原始异常。这与Java finally块中的行为完全相同。此外,您不能在try&amp; catch块中使用return

答案 10 :(得分:3)

我想出了一个finally宏,可以在Java中使用几乎像¹finally关键字;它使用std::exception_ptr和朋友,lambda函数和std::promise,因此它需要C++11或更高;它还使用了Consumer GCC扩展,这也得到了clang的支持。

警告:此答案的compound statement expression使用了该概念的不同实现,但有更多限制。

首先,让我们定义一个助手类。

#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);
}

然后就是实际的宏。

#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所需的功能即可避免。

¹ CAVEAT:有些内容与finally的java版本不同。在我的头顶:

  1. 不可能在breaktry的块中使用catch()语句从外部循环中断,因为它们位于lambda函数中;
  2. catch()之后必须至少有一个try块:这是C ++的要求;
  3. 如果函数的返回值不是void,但trycatch()'s块内没有返回,则编译将失败,因为finally宏将扩展为需要的代码返回void。通过使用finally_noreturn宏的排序,这可能是错误的 void
  4. 总而言之,我不知道自己是否会使用这些东西,但玩它很有趣。 :)

答案 11 :(得分:2)

我有一个用例,我认为finally 应该是C ++ 11语言中完全可以接受的部分,因为我认为它更容易从流程点读取视图。我的用例是消费者/生产者线程链,其中在运行结束时发送了一个标记nullptr以关闭所有线程。

如果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);
        }
    }

我认为将你的finally声明放在循环的开头是更合乎逻辑的,因为它发生在循环退出之后......但这是一厢情愿的想法,因为我们不能在C ++中这样做。请注意,队列downstream已连接到另一个线程,因此您无法在push(nullptr)的析构函数中放入sentinel downstream,因为此时无法将其销毁。它需要保持活着,直到另一个线程收到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);
        }
    }

答案 12 :(得分:1)

正如许多人所说,解决方案是使用C ++ 11功能来避免最终阻塞。其中一项功能是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);
}

使用unique_ptr和C ++标准库容器的更多介绍是here

答案 13 :(得分:1)

我想提供另一种选择。

如果你想要始终调用finally块,只需将它放在最后一个catch块之后(可能应该catch( ... )来捕获未知异常)

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(); 

如果你想在抛出任何异常时将finally块作为最后一件事你可以使用boolean局部变量 - 在运行之前你将它设置为false并在try块的最后放置true赋值,然后在catch块检查之后对于变量值:

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();
}

答案 14 :(得分:-1)

try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}

答案 15 :(得分:-1)

我还认为,RIIA并不是异常处理和finally的完全有用的替代品。顺便说一句,我也认为RIIA到处都是个坏名字。我将这些类型的类称为“管理员”,并使用它们很多。 95%的时间他们既不初始化也不获取资源,他们在范围内应用某些更改,或者采用已经设置的内容并确保其已被销毁。这是困扰互联网的正式名称,我甚至因为暗示我的名字可能更好而被滥用。

我只是不认为要求某些临时清单的每个复杂设置都必须编写一个类来包含它,以免在清理所有需要面对的备份时避免复杂化,这是不合理的如果在处理过程中出现问题,则可以捕获多种异常类型。这将导致很多临时类,否则就没有必要了。

是的,对于设计用于管理特定资源的类或设计用于处理一组相似资源的通用类来说,这是很好的。但是,即使所涉及的所有事物都具有此类包装器,清除的协调也可能不仅仅是销毁器反向调用的简单方法。

我认为C ++拥有一个final是很有意义的。我的意思是,老兄,在过去的几十年里,它粘在了很多东西上,似乎奇怪的人们突然对像最终这样的东西变得保守了,这可能是非常有用的,可能没有其他东西那么复杂添加了(尽管这只是我的猜测。)