【问题标题】:Are there cases where a "finally" construct would be useful in C++?是否存在“最终”构造在 C++ 中有用的情况?
【发布时间】:2008-12-21 22:05:56
【问题描述】:

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) 更好?

【问题讨论】:

  • 您在问两个不同的问题。标题问为什么没有 finally,而实际帖子要求提供 finally 比 RAII 更好的案例示例。我建议您编辑其中一个,以便它们实际上匹配。在搜索中偶然发现这篇文章的人会感到困惑。

标签: c++ raii finally


【解决方案1】:

它们之间的区别在于析构函数通过将清理解决方案与所使用的类型相关联来强调对清理解决方案的重用,而 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++ 传统上强调通过命名类型表达的“强制可重用性”。

【讨论】:

  • 在没有语言结构的情况下最终实现的好主意。但是即使 tr() 没有抛出,try_finally 也应该调用 fi()。
  • 还将一些文本移到顶部,因此它实际上也回答了问题。
【解决方案2】:

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

【讨论】:

  • 小错字。应该是 RIIA_Resource 包装器;而不是 RIIA_Resource wrapper(); .你可以做 auto_ptr const p(aquire_resource()); auto_ptr 关心所有的东西本身。我知道这只是一个例子 :) 所以在你纠正错字并提到智能指针后,你会得到 +1 :)
  • 糟糕。当我开始编写它时,我将资源传递给构造函数,并将其存储在引用而不是指针中。很好的收获。
  • 显然我的班级使用“资源初始化即分配”模式。谢谢。
  • 好吧,它对我有用,只要它不是 RIAA。 ;)(当然是资源初始化和分配)
  • 另外 - 查找 scope_guard - 声明一次,它允许您设置在离开范围时调用的任意函数。
【解决方案3】:

最后用 C 代码连接会更好。必须在 RAII 中包装现有的 C 功能可能会很痛苦。

【讨论】:

    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

      我发现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++ 要求更多。它已经非常适合这个目的。但是,在任何一开始就允许它们发生的语言中,回滚外部副作用总是很困难的,我总是很感激任何一点点帮助来使这种情况变得更容易。

      【讨论】:

        【解决方案6】:

        六个答案后编辑。

        这个呢:

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

        【讨论】:

        • 如果你的 catch 表达式抛出另一个异常,那么永远不会调用 delete。
        • 对象 *myObject = 0;尝试 { myObject = new Object(); // 抛出异常 } catch (exception &e) { try{ // 处理异常 } catch {} } delete myObject;问题解决了:-)
        • 因为在大多数情况下,您不想捕获异常。如果你确实抓住了它,你几乎应该总是重新扔掉它。
        • 这可能是我见过的最丑陋的编程黑客。你甚至可以在 C++ 中做 typeof 吗?
        • 确实很难看,而且 RTTI 调用(您可以在 C++ 中执行)非常昂贵,因此我认为这也不是解决方案。也许您可以在 what() 函数中添加一些内容,这将使其性能更高,但仍然很难看。 :P
        猜你喜欢
        • 1970-01-01
        • 2017-02-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多