c++支持“finally”块吗?

RAII习语是什么?

c++的RAII习语和c#的using语句有什么区别?


当前回答

除了使基于堆栈的对象的清理变得容易之外,RAII还很有用,因为当对象是另一个类的成员时,同样的“自动”清理也会发生。当所属类被销毁时,由RAII类管理的资源将被清理,因为该类的dtor将被调用。

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

其他回答

不,c++不支持'finally'块。原因是c++支持RAII:“资源获取是初始化”——对于一个真正有用的概念来说,这是一个糟糕的名字。

其思想是,对象的析构函数负责释放资源。当对象具有自动存储持续时间时,当创建对象的块退出时,对象的析构函数将被调用——即使该块在出现异常时退出。以下是Bjarne Stroustrup对这个话题的解释。

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 also simplifies using objects as members of other classes. When the owning class' is destructed, the resource managed by the RAII class gets released because the destructor for the RAII-managed class gets called as a result. This means that when you use RAII for all members in a class that manage resources, you can get away with using a very simple, maybe even the default, destructor for the owner class since it doesn't need to manually manage its member resource lifetimes. (Thanks to Mike B for pointing this out.)

For those familliar with C# or VB.NET, you may recognize that RAII is similar to .NET deterministic destruction using IDisposable and 'using' statements. Indeed, the two methods are very similar. The main difference is that RAII will deterministically release any type of resource -- including memory. When implementing IDisposable in .NET (even the .NET language C++/CLI), resources will be deterministically released except for memory. In .NET, memory is not deterministically released; memory is only released during garbage collection cycles.

 

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

总之,微软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调试设置会破坏这种机制。

我想出了一个finally宏,可以像¹Java中的finally关键字一样使用;它使用std::exception_ptr及其友项,lambda函数和std::promise,因此它要求c++ 11或以上;它还使用了clang也支持的复合语句表达式GCC扩展。

警告:这个答案的早期版本使用了这个概念的不同实现,有更多的限制。

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

#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中重新实现所需的功能来避免。


注意:有一些东西不像java版本的finally那样工作。我能想到的是:

it's not possible to break from an outer loop with the break statement from within the try and catch()'s blocks, since they live within a lambda function; there must be at least one catch() block after the try: it's a C++ requirement; if the function has a return value other than void but there's no return within the try and catch()'s blocks, compilation will fail because the finally macro will expand to code that will want to return a void. This could be, err, avoided by having a finally_noreturn macro of sorts.

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

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

此外,c++核心指南最后给出了。

这里有一个到GSL微软实现的链接和一个到Martin Moene实现的链接

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和delete),并且在c++中按照后进先出(LIFO)的顺序进行构造。唯一的缺点是你得到了一个你并不真正使用的auto变量,lambda语法使它有点嘈杂(在我的例子中,在第四行中,只有单词finally和右边的{}块是有意义的,其余的基本上都是嘈杂的)。

另一个例子:

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

如果只有在失败的情况下才必须调用finally,则disable成员非常有用。例如,你必须在三个不同的容器中复制一个对象,你可以设置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"; }

希望你能使用c++ 11,这段代码更多地是为了表明“c++最终不支持”是如何从c++的最初几周开始就毫无意义的,甚至在c++得到它的名字之前就可以编写这种代码。

不一定,但你可以在一定程度上模仿他们,例如:

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;

注意,final -block本身可能在原始异常被重新抛出之前抛出一个异常,从而丢弃原始异常。这与Java final -block中的行为完全相同。同样,你不能在try&catch块中使用return。