提问人:Kevin 提问时间:10/2/2008 最后编辑:MottiKevin 更新时间:11/6/2020 访问量:306535
C++ 是否支持“finally”块?(我一直听说的这个“RAII”是什么?
Does C++ support 'finally' blocks? (And what's this 'RAII' I keep hearing about?)
答:
不,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 还简化了将对象用作其他类的成员的过程。当所属类被解构时,将释放由 RAII 类管理的资源,因为 RAII 托管类的析构函数将因此被调用。这意味着,当您将 RAII 用于管理资源的类中的所有成员时,您可以为所有者类使用非常简单的(甚至是默认的)析构函数,因为它不需要手动管理其成员资源生存期。(感谢 Mike B 指出这一点。
对于熟悉 C# 或 VB.NET 的用户,您可能会认识到 RAII 类似于使用 IDisposable 和 'using' 语句的 .NET 确定性销毁。事实上,这两种方法非常相似。主要区别在于 RAII 将确定性地释放任何类型的资源——包括内存。在 .NET(甚至是 .NET 语言 C++/CLI)中实现 IDisposable 时,将确定性地释放资源,但内存除外。在 .NET 中,内存不会确定性释放;内存仅在垃圾回收周期中释放。
† 有些人认为“破坏就是资源放弃”是 RAII 成语的更准确名称。
评论
FWIW,Microsoft Visual C++ 确实支持 try,finally,它历来在 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 调试设置会破坏此机制。
评论
除了使用基于堆栈的对象使清理变得容易之外,RAII 也很有用,因为当对象是另一个类的成员时,也会发生相同的“自动”清理。当所属类被销毁时,RAII 类管理的资源将被清理,因为该类的 dtor 因此被调用。
这意味着,当您达到 RAII 涅槃并且类中的所有成员都使用 RAII(如智能指针)时,您可以为所有者类使用非常简单(甚至可能是默认的)dtor,因为它不需要手动管理其成员资源生存期。
评论
在 C++ 中,由于 RAII,不需要 finally。
RAII 将异常安全的责任从对象的用户转移到对象的设计者(和实现者)。我认为这是正确的地方,因为您只需要获得一次异常安全性(在设计/实现中)。通过使用 finally,您需要在每次使用对象时获得正确的异常安全性。
此外,IMO 代码看起来更整洁(见下文)。
例:
数据库对象。要确保使用数据库连接,必须打开和关闭它。通过使用 RAII,这可以在构造函数/析构函数中完成。
类似 RAII 的 C++
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 终于
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
评论
// Make sure not to throw exception if one is already propagating.
由于这个原因,C++ 析构函数也不要抛出异常,这一点很重要。
try
{
...
goto finally;
}
catch(...)
{
...
goto finally;
}
finally:
{
...
}
评论
finally
很抱歉挖出这么旧的线程,但以下推理存在重大错误:
RAII 将异常安全的责任从对象的用户转移到对象的设计者(和实现者)。我认为这是正确的地方,因为您只需要获得一次异常安全性(在设计/实现中)。通过使用 finally,您需要在每次使用对象时获得正确的异常安全性。
通常情况下,您必须处理动态分配的对象、动态数量的对象等。在 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();
}
}
另外:为什么即使是托管语言也提供最终块,尽管资源无论如何都会由垃圾回收器自动释放?
提示:你可以用“finally”做更多的事情,而不仅仅是内存释放。
评论
new
不返回 NULL,而是抛出异常
std::shared_ptr
std::unique_ptr
为什么即使是托管语言也提供了一个 finally 块,尽管资源无论如何都会由垃圾回收器自动释放?
实际上,基于垃圾收集器的语言“最终”需要更多。垃圾回收器不会及时销毁您的对象,因此不能依靠它来正确清理与内存无关的问题。
就动态分配的数据而言,许多人会争辩说您应该使用智能指针。
然而。。。
RAII 将异常安全的责任从对象的用户转移到设计者
可悲的是,这是它自己的垮台。旧的 C 编程习惯很难改变。当您使用用 C 或非常 C 风格编写的库时,不会使用 RAII。除了重写整个 API 前端之外,这正是您必须使用的。然后缺乏“最后”真的很咬人。
评论
CleanupFailedException
SomeObject.DoSomething()
不是真的,但你可以在一定程度上模仿它们,例如:
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-block 本身可能会在重新抛出原始异常之前抛出异常,从而丢弃原始异常。这与 Java finally-block 中的行为完全相同。此外,您不能在 try&catch 块中使用。return
评论
std::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
正如许多人所说,解决方案是使用 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++ 标准库容器配合使用
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 文件描述符。
拥有一个管理资源并避免任何类型的泄漏的真正类通常更好,但这最终在创建类听起来像是矫枉过正的情况下很有用。
此外,我最终比其他语言更喜欢它,因为如果自然使用,您可以在开始代码附近编写关闭代码(在我的示例中是 new 和 delete),并且销毁遵循 C++ 中通常的 LIFO 顺序构造。唯一的缺点是你得到一个你并不真正使用的自动变量,并且 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,你仍然可以使用 finally,但代码变得有点冗长。只需定义一个只有构造函数和析构函数的结构,构造函数引用任何需要的东西,析构函数执行您需要的操作。这基本上就是 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++ 得名之前就有可能编写这种代码。
评论
FinalAction
ScopeGuard
编辑
如果您不中断/继续/返回等,您可以向任何未知异常添加一个 catch,并将 always 代码放在它后面。这也是您不需要重新抛出异常的时候。
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();
那么问题出在哪里呢?
通常,在其他编程语言中,通常无论如何都会运行(通常意味着无论任何返回、中断、继续......除了某种系统 - 每种编程语言差异很大 - 例如.PHP和Java在那一刻退出,但Python最终还是执行然后退出。exit()
但是我上面描述的代码不能以这种方式
工作=>以下代码仅输出: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;
}
评论
我有一个用例,我认为应该是 C++11 语言中完全可以接受的一部分,因为我认为从流程的角度来看它更容易阅读。我的用例是线程的消费者/生产者链,在运行结束时发送哨兵以关闭所有线程。finally
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)
downstream
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);
}
}
评论
我想出了一个宏,它几乎可以像 Java 中的关键字一样使用¹;它利用了 和 friends、lambda 函数和 ,所以它需要或更高;它还使用了复合语句表达式 GCC 扩展,CLANG 也支持该扩展。finally
finally
std::exception_ptr
std::promise
C++11
警告:此答案的早期版本使用了该概念的不同实现,但具有更多限制。
首先,让我们定义一个帮助程序类。
#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
- 不可能在 和 的块中使用语句从外部循环中断,因为它们位于 lambda 函数中;
break
try
catch()
- 之后必须至少有一个块:这是 C++ 要求;
catch()
try
- 如果函数的返回值不是 void,但在 and 块中没有返回值,则编译将失败,因为宏将扩展为要返回 .这可能是,呃,由于拥有某种宏而产生的空白。
try
catch()'s
finally
void
finally_noreturn
总而言之,我不知道我自己是否会使用这些东西,但玩它很有趣。:)
评论
catch(xxx) {}
finally
catch(...)
xxx
finally.cpp:60:5: error: ‘...’ handler must be the last handler for its try block
catch(...)
另一个使用 C++11 lambda 函数的“finally”块仿真
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
}
);
如果你愿意,你可以把这个成语包装成“try - finally”宏:
// 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 :(
现在 “finally” 块在 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
就我个人而言,我不喜欢“finally”成语的“宏”版本,并且更喜欢使用纯“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(); });
...
正如其他答案中指出的,C++可以支持类似的功能。此功能的实现可能最接近标准语言的一部分,是 C++ Core Guidelines 附带的实现,这是一组使用 C++ 的最佳实践,由 Bjarne Stoustrup 和 Herb Sutter 编辑。finally
的实现是指南支持库 (GSL) 的一部分。在整个准则中,建议在处理旧式接口时使用 of,并且它也有自己的准则,标题为“如果没有合适的资源句柄可用,则使用final_action对象来表示清理”。finally
finally
因此,不仅 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 的回答非常相似。一个区别是 创建的对象缺少调用。如果你需要这个功能(比如说,在资源组装好后返回资源,并且不会发生异常),你可能更喜欢 Paolo 的实现。否则,使用 GSL 与使用标准化功能一样接近。gsl::finally()
disable()
我还认为 RIIA 并不是异常处理和 finally 的完全有用的替代品。顺便说一句,我也认为RIIA是一个坏名字。我称这些类型的课程为“看门人”,并经常使用它们。95% 的时间,他们既不是初始化也不是获取资源,而是在范围的基础上应用一些更改,或者采用已经设置的东西并确保它被销毁。这是官方模式名称痴迷的互联网,我甚至因为建议我的名字可能更好而受到滥用。
我只是不认为要求某些临时事物列表的每个复杂设置都必须编写一个类来包含它是不合理的,以避免在清理所有备份时出现复杂情况,因为如果在此过程中出现问题,则需要捕获多个异常类型。这将导致许多临时类,否则这些类就没有必要了。
是的,对于旨在管理特定资源的类或旨在处理一组类似资源的泛型类来说,这很好。但是,即使所有涉及的事情都有这样的包装器,清理的协调也可能不仅仅是简单的反序调用析构函数。
我认为 C++ 有一个 finally 是完全有意义的。我的意思是,天哪,在过去的几十年里,有太多的零碎的东西被粘在上面,以至于似乎奇怪的人们会突然对像 finally 这样的东西变得保守,这可能非常有用,而且可能没有像其他一些添加的东西那么复杂(尽管这只是我的猜测。
评论