Bjarne Stroustrup在他的C++ Style and Technique FAQ写道,强调我的:
因为C ++支持 几乎总是更好 的替代方案:“资源获取是初始化”技术(TC ++ PL3第14.4节)。基本思想是通过本地对象表示资源,以便本地对象的析构函数将释放资源。这样,程序员就不会忘记释放资源。例如:
class File_handle { FILE* p; public: File_handle(const char* n, const char* a) { p = fopen(n,a); if (p==0) throw Open_error(errno); } File_handle(FILE* pp) { p = pp; if (p==0) throw Open_error(errno); } ~File_handle() { fclose(p); } operator FILE*() { return p; } // ... }; void f(const char* fn) { File_handle f(fn,"rw"); // open fn for reading and writing // use file through f }
在系统中,我们需要为每个资源提供“资源句柄”类。但是,我们不必为每次获取资源都有一个“finally”子句。在现实系统中,资源获取比资源种类多得多,因此“资源获取是初始化”技术导致的代码少于使用“最终”构造的代码。
请注意,Bjarne写的“几乎总是更好”而不是“总是更好”。现在我的问题是:finally
构造比在C ++中使用替代构造(RAII)更好吗?
答案 0 :(得分:7)
它们之间的区别在于析构函数强调通过将清除解决方案与正在使用的类型相关联来重用清理解决方案,而try / finally则强调一次性清理例程。因此,当您具有与使用点相关联的唯一一次性清理要求时,尝试/最终会更方便,而不是可以与您正在使用的类型相关联的可重复使用的清理解决方案。
我没有试过这个(几个月没有下载最近的gcc),但它应该是真的:通过在语言中添加lambdas,C ++现在可以有效等效finally
,只需编写一个名为try_finally
的函数。明显的用法:
try_finally([]
{
// attempt to do things in here, perhaps throwing...
},
[]
{
// this always runs, even if the above block throws...
}
当然,你必须写一下try {finally
,但只有一次然后你就可以了。 Lambdas启用了新的控制结构。
类似的东西:
template <class TTry, class TFinally>
void try_finally(const TTry &tr, const TFinally &fi)
{
try
{
tr();
}
catch (...)
{
fi();
throw;
}
fi();
}
在GC的存在和try / finally的偏好而不是析构函数之间根本没有链接。 C ++ / CLI具有析构函数和GC。他们是正交选择。尝试/最终和析构函数是针对同一问题的略有不同的解决方案,两者都是非可替代资源所需的确定性问题。
C ++函数对象强调可重用性,但使一次性匿名函数变得痛苦。通过添加lambda,匿名代码块现在很容易实现,这避免了C ++传统上强调通过命名类型表达的“强制可重用性”。
答案 1 :(得分:6)
最后连接C代码会更好。在RAII中包装现有的C功能可能会很痛苦。
答案 2 :(得分:6)
我能想到最后一个块会“更好”的唯一原因是它需要更少的代码来完成同样的事情。例如,如果您有一个资源,由于某种原因不使用RIIA,您可能需要编写一个类来包装资源并在析构函数中释放它,或者使用finally块(如果它存在)。 / p>
比较
class RAII_Wrapper
{
Resource *resource;
public:
RAII_Wrapper() : resource(aquire_resource()) {}
~RAII_Wrapper() {
free_resource(resource);
delete resource;
}
Resource *getResource() const {
return resource;
}
};
void Process()
{
RAII_Resource wrapper;
do_something(wrapper.resource);
}
与
void Process()
{
try {
Resource *resource = aquire_resource();
do_something(resource);
}
finally {
free_resource(resource);
delete resource;
}
}
大多数人(包括我)仍然认为第一个版本更好,因为它不会强迫你使用try ... finally块。您还只需要编写一次类,而不是在使用该资源的每个函数中复制代码。
编辑:与上面提到的litb一样,你应该使用auto_ptr而不是手动删除指针,这样可以简化这两种情况。
答案 3 :(得分:3)
我认为scope guard在处理最终处理良好的一次性案例方面做得很好,而在更一般意义上更好,因为它可以很好地处理多个流路径。
答案 4 :(得分:1)
我在finally
找到的主要用途是处理C代码,因为其他人指出C资源可能只在代码中使用一次或两次而不值得包装到RAII中 - 符合结构。也就是说,使用lambdas,通过调用我们在函数本身中指定的函数对象来调用一些自定义逻辑似乎很容易。
我发现的另一个用例是针对异常的杂项代码,无论我们是否处于正常或异常的执行路径中都应该执行,例如打印时间戳或退出函数时无论什么。这对于我来说是如此罕见的情况,仅仅为它提供语言功能似乎有点过分了,现在使用lambdas仍然很容易做到,而不必为此专门编写一个单独的类目的
在大多数情况下,我发现它的使用情况非常有限,现在似乎并没有真正证明这种语言发生如此大的变化。我的小梦想是通过正常的执行路径或特殊的路径来判断物体是否被摧毁的一些方法。
这样可以简化范围保护,不再需要commit/dismiss
调用来接受更改,而不会在范围保护被销毁时自动回滚。这个想法是允许这个:
ScopeGuard guard(...);
// Cause external side effects.
...
// If we managed to reach this point without facing an exception,
// dismiss/commit the changes so that the guard won't undo them
// on destruction.
guard.dismiss();
简单地说:
ScopeGuard guard(...);
// Cause external side effects.
...
我总是发现需要解雇守卫有点尴尬和容易出错,因为我有时会忘记解雇他们只是为了让他们撤消所有的变化,让我摸不着头脑关于为什么我的操作似乎什么都不做,直到我意识到,&#34; oops,我忘了解雇范围后卫。&#34; 。这是一个小问题,但大多数情况下,我会发现它更加优雅,以消除明确范围保护解雇的需要,如果他们可以在他们的析构函数中告诉他们是否通过正常方式销毁它们是可能的执行路径(此时应保留副作用)或特殊情况(此时副作用应撤消)。
这是最小的事情,但是在最困难的例外安全领域才能做到正确:回滚外部副作用。当涉及到恰当地破坏本地资源时,我无法从C ++中要求更多。它已经非常适合这个目的。但是,在任何允许它们首先出现的语言中,回击外部副作用一直很困难,而且任何一点点的帮助都可以让我更容易这样做,这是我一直很欣赏的。
答案 5 :(得分:0)
在六个答案后编辑。
这个怎么样:
class Exception : public Exception { public: virtual bool isException() { return true; } };
class NoException : public Exception { public: bool isException() { return false; } };
Object *myObject = 0;
try
{
try
{
myObject = new Object(); // Create an object (Might throw exception)
}
catch (Exception &e)
{
// Do something with exception (Might throw if unhandled)
}
throw NoException();
}
catch (Exception &e)
{
delete myObject;
if (e.isException()) throw e;
}