C++是否支持'finally'块?(我一直听到的这个"RAII"是什么?

Does C++ support 'finally' blocks? (And what's this 'RAII' I keep hearing about?)

本文关键字:RAII 是什么 支持 是否 finally C++ 一直      更新时间:2023-10-16

C++是否支持'final'块?

什么是 RAII 成语

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

不,C++不支持"final"块。 原因是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 还简化了将对象用作其他类的成员的过程。当拥有类'被析构时,RAII 类管理的资源将被释放,因为 RAII 管理的类的析构函数因此被调用。 这意味着,当您对管理资源的类中的所有成员使用 RAII 时,您可以避免使用 owner 类的非常简单的析构函数,甚至是默认析构函数,因为它不需要手动管理其成员资源生存期。(感谢Mike B指出这一点。

对于那些熟悉 C# 或 VB.NET 的人来说,您可能会认识到 RAII 类似于使用 IDisposable 和"using"语句的 .NET 确定性销毁。 事实上,这两种方法非常相似。 主要区别在于RAII将确定性地释放任何类型的资源 - 包括内存。 在 .NET 中实现 IDisposable 时(甚至是 .NET 语言 C++/CLI),将确定性地释放除内存之外的资源。 在 .NET 中,内存不会确定性地释放;内存仅在垃圾回收周期期间释放。

 

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

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

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

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

例:

数据库对象。要确保使用数据库连接,必须打开和关闭它。通过使用 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 对象将使用析构函数正确关闭自身,无论我们如何尝试滥用它。

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

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

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

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

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

此外,C++核心准则最后给出了。

这是GSL Microsoft实现的链接和Martin Moene实现的链接

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

如果需要,您可以轻松实现自己,请继续阅读。

在 C++11 中,RAII 和 lambda 允许最后做一个一般:

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文件描述符。

拥有一个真正的类来管理资源并因此避免任何类型的泄漏通常更好,但这最终在制作类听起来像是矫枉过正的情况下很有用。

此外,我最终比其他语言更喜欢它,因为如果自然使用,您将关闭代码写在打开代码附近(在我的示例中为 newdelete),销毁遵循 LIFO 顺序,如往常一样在C++中。唯一的缺点是你得到一个你并不真正使用的自动变量,并且lambda语法使它有点嘈杂(在我的第四行的例子中,只有单词final和右侧的{}块是有意义的,其余的基本上是噪音)。

再比如:

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

如果只有在失败时才必须调用 final,则禁用成员很有用。例如,您必须在三个不同的容器中复制一个对象,您可以设置 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++得名之前就有可能编写这种代码。

为什么即使托管语言也提供最终块,尽管垃圾回收器会自动释放资源?

实际上,基于垃圾收集器的语言"最终"需要更多。垃圾回收器不会及时销毁对象,因此不能依靠它来正确清理与内存无关的问题。

就动态分配的数据而言,许多人会争辩说你应该使用智能指针。

然而。。。

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

可悲的是,这是它自己的垮台。 旧的C编程习惯很难改变。 当您使用用 C 或非常 C 样式编写的库时,不会使用 RAII。除了重写整个 API 前端之外,这就是您必须使用的。 那么缺乏"终于"真的咬人了。

除了使用基于堆栈的对象简化清理之外,RAII 也很有用,因为当对象是另一个类的成员时,也会发生相同的"自动"清理。 当所属类被销毁时,RAII 类管理的资源将被清理,因为该类的 dtor 因此被调用。

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

另一个

使用 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
    }
);

如果你愿意,你可以把这个成语包装成"尝试 - 最后"宏:

// 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 :(

现在"终于"块在 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

就个人而言,我不喜欢"最终"成语的"宏"版本,并且更喜欢使用纯"with_finally"函数,即使在这种情况下语法更笨重。

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

附言

如果你需要在代码中使用 finally 块,那么作用域保护或 ON_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(); });
    ...

很抱歉挖出了这么旧的线程,但以下推理存在重大错误:

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

通常情况下,您必须处理动态分配的对象、动态数量的对象等。在 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();
  }
}

另外:为什么即使托管语言也提供最终块,尽管垃圾回收器会自动释放资源?

提示:您可以使用"最终"做更多的事情,而不仅仅是内存释放。

FWIW,Microsoft Visual C++确实支持try,final,它历来在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 调试设置会破坏此机制。

正如其他答案所指出的,C++可以支持类似finally的功能。 此功能的实现可能最接近于成为标准语言的一部分,是C++核心指南随附的实现,这是一组使用C++最佳实践,由Bjarne Stoustrup和Herb Sutter编辑。 finally的实施是指南支持库 (GSL) 的一部分。 在整个指南中,建议在处理旧式接口时使用finally,并且它还有自己的指南,标题为"如果没有合适的资源句柄,则使用 final_action 对象表示清理"。

因此,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的实现和使用与Paolo.Bolzoni的答案非常相似。 一个区别是 gsl::finally() 创建的对象缺少disable()调用。 如果您需要该功能(例如,在资源组装后返回资源并且必然不会发生异常),您可能更喜欢 Paolo 的实现。 否则,使用 GSL 与使用标准化功能一样接近。

我有一个用例,我认为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连接到另一个线程,因此您无法在 downstream 的析构函数中放入哨兵push(nullptr),因为此时无法销毁它......它需要保持活动状态,直到另一个线程收到nullptr

因此,以下是将 RAII 类与 lambda 一起使用来执行相同操作的方法:

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

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

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 final-block中的行为完全相同。此外,您不能在 try&catch 块中使用return

我想出了一个finally宏,几乎可以像Java中的finally关键字¹一样使用;它利用std::exception_ptr和朋友,lambda函数和std::promise,所以它需要C++11或以上;它还利用了复合语句表达式GCC扩展,这也得到了clang的支持。

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

首先,让我们定义一个帮助程序类。

#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 那样工作。在我的头顶上:

    不可能在
  1. trycatch() 块中使用 break 语句从外部循环中断,因为它们存在于 lambda 函数中;
  2. try后必须至少有一个catch()块:这是一个C++要求;
  3. 如果函数的返回值不是 void,但在 trycatch()'s 块中没有返回,则编译将失败,因为finally宏将扩展到想要返回void的代码。这可能是,错误,通过拥有某种finally_noreturn宏而无效。

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

正如许多人所说,解决方案是使用 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++ 标准库容器一起使用的更多介绍,请参阅此处

我也认为 RIIA 不是异常处理和最终的完全有用的替代品。顺便说一句,我也认为RIIA是一个坏名字。我称这些类型的课程为"看门人",并经常使用它们。95% 的时间,他们既不初始化也不获取资源,而是在限定范围内应用一些更改,或者采用已经设置的内容并确保它被销毁。这是官方模式名称痴迷的互联网,我甚至因为暗示我的名字可能更好而受到滥用。

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

是的,对于旨在管理特定资源的类或旨在处理一组类似资源的泛型类,这很好。但是,即使所有涉及的事情都有这样的包装器,清理的协调可能不仅仅是对析构函数的简单反向顺序调用。

我认为C++最终有一个是完全有意义的。我的意思是,哎呀,在过去的几十年里,已经粘在了太多的零碎东西上,似乎奇怪的人会突然变得保守,因为 finally 可能非常有用,而且可能没有其他一些东西那么复杂已经添加(尽管这只是我的猜测。

已编辑

如果您没有中断/继续/返回等,则可以向任何未知异常添加捕获并将始终代码放在其后面。这也是您不需要重新抛出异常的时候。

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

那么问题出在哪里呢?

通常,

在其他编程语言中,最终通常无论如何运行(通常意味着无论任何返回,中断,继续,...),除了某种系统exit() - 每种编程语言差异很大 - 例如.PHP和Java只是在那一刻退出,但Python最终执行然后退出。

但是我上面描述的代码不是这样工作的
=> 以下代码输出something wrong!

#include <stdio.h>
#include <iostream>
#include <string>
std::string test() {
    try{
       // something that might throw exception
       throw "exceptiooon!";
       return "fine";
    } catch( ... ){
       return "something wrong!";
    }
    
    return "finally";
}
int main(void) {
    
    std::cout << test();
    
    
    return 0;
}
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}