6

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”子句。在现实系统中,资源获取远多于资源种类,因此“资源获取即初始化”技术导致的代码比使用“finally”构造更少。

请注意,Bjarne 写的是“几乎总是更好”而不是“总是更好”。现在我的问题是:在什么情况下finally构造比在 C++ 中使用替代构造 (RAII) 更好?

4

6 回答 6

7

它们之间的区别在于析构函数通过将清理解决方案与所使用的类型相关联来强调对清理解决方案的重用,而 try/finally 则强调一次性清理例程。因此,当您有与使用点相关的唯一一次性清理要求时,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,但只有一次,然后你就可以开始了。Lambda 支持新的控制结构。

就像是:

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。它们是正交的选择。Try/finally 和析构函数是对同一问题的稍微不同的解决方案,都是确定性的,需要不可替代的资源。

C++ 函数对象强调可重用性,但使一次性匿名函数很痛苦。通过添加 lambda,匿名代码块现在很容易制作,这避免了 C++ 传统上强调通过命名类型表达的“强制可重用性”。

于 2008-12-21T22:33:32.913 回答
6

finally 与 C 代码连接时会更好。必须将现有的 C 功能包装在 RAII 中可能会很痛苦。

于 2008-12-21T22:19:34.610 回答
6

我能想到 finally 块会“更好”的唯一原因是它需要更少的代码来完成同样的事情。例如,如果你有一个资源,由于某种原因不使用 RAII,你要么需要编写一个类来包装资源并在析构函数中释放它,要么使用 finally 块(如果它存在)。

比较:

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 而不是手动删除指针,这将简化这两种情况。

于 2008-12-21T22:31:46.177 回答
3

我认为作用域守卫在处理最终处理良好的一次性案例方面做得很好,同时在更一般的意义上更好,因为它可以很好地处理多个流路径。

于 2008-12-22T14:45:25.383 回答
1

我发现的主要用途finally是在处理 C 代码时,正如其他人指出的那样,C 资源可能只在代码中使用一次或两次,并且不值得包装到符合 RAII 的结构中。也就是说,使用 lambdas,通过 dtor 调用我们在函数本身中指定的函数对象来调用一些自定义逻辑似乎很容易。

我发现的另一个用例是异国杂项代码,无论我们处于正常还是异常执行路径,都应该执行,例如打印时间戳或无论如何退出函数。这是一个非常罕见的情况,但对我来说,仅仅为它提供一个语言特性似乎有点过头了,而且现在使用 lambdas 仍然很容易,而不必为此编写一个单独的类。

在大多数情况下,我现在发现它的用例非常有限,而这些用例似乎并不能真正证明对语言进行如此大的改变是合理的。不过,我的小梦想是通过某种方式在对象的 dtor 内部判断该对象是通过正常执行路径还是异常执行路径被破坏。

这将简化作用域保护,不再需要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.
...

我总是发现解除范围守卫的必要性有点尴尬而且容易出错,因为我有时忘记解除它们只是为了让它们撤消所有更改,让我摸不着头脑为什么我的手术似乎什么也没做,直到我意识到,“哎呀,我忘了解散范围守卫。” . 这是一件小事,但大多数情况下,我会发现消除显式范围保护解除的需要更加优雅,如果他们可以在析构函数中判断它们是否通过正常执行路径被销毁(此时点应该保留副作用)或特殊的(此时应该撤消副作用)。

这是最次要的事情,但在异常安全最难的领域中正确处理:回滚外部副作用。当涉及到正确地破坏本地资源时,我不能对 C++ 要求更多。它已经非常适合该目的。但是,在任何一开始就允许它们发生的语言中,回滚外部副作用总是很困难的,而且任何一点点帮助都可以让这种情况变得更容易,我总是很感激。

于 2018-01-05T13:21:03.920 回答
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;
}
于 2008-12-21T23:19:11.383 回答