13

多年来一直在做 Java,所以一直没有跟踪 C++。语言定义中的 C++ 异常处理中是否添加了finally子句?

是否有模仿 Java 的 try/finally 的流行习语?

我还担心 C++ 没有针对所有可能抛出的异常的终极超类型——比如 Java 的 Throwable 类。

我可以写:

try {
  // do something
} catch(...) {
  // alas, can't examine the exception
  // can only do cleanup code and perhaps rethrow, ala:
  throw;
}

附录编辑:

我最终接受了投票最多的答案,即使用析构函数进行清理。当然,从我自己的评论来看,很明显我并不完全同意这一点。然而,C++ 就是这样,所以在我想到的应用程序努力中,我将或多或少地努力坚持共同的社区实践。我将使用模板类来包装还没有类析构函数的资源(即 C 库资源),从而赋予它们析构函数语义。

新附录编辑:

嗯,而不是finally然后是一个关闭功能?结合 ScopeGuard 方法的闭包(请参阅下面的答案之一)将是一种通过任意操作完成清理并访问清理代码的外部范围上下文的方法。清理可以以 Ruby 编程中的惯用方式完成,在打开资源时它们提供清理块。是否没有考虑 C++ 的闭包功能?

4

15 回答 15

26

通过有效地使用析构函数。当 try 块中抛出异常时,在其中创建的任何对象都将立即被销毁(并因此调用其析构函数)。

这与 Java 不同,在 Java 中您不知道何时调用对象的终结器。

更新:直接从马的嘴里: 为什么 C++ 不提供“最终”构造?

于 2009-02-01T05:10:28.440 回答
15

我的 .02 美元。多年来,我一直在使用 C# 和 Java 等托管语言进行编程,但为了提高速度,我不得不切换到 C++。起初我不敢相信我必须在头文件和 cpp 文件中写出两次方法签名,我不喜欢没有 finally 块,没有垃圾收集意味着到处跟踪内存泄漏 -天哪,我一点也不喜欢!

但是,正如我所说,我被迫使用 C++。所以我被迫认真学习它,现在我终于理解了所有的编程习惯,比如 RAII,我了解了语言的所有微妙之处等等。我花了一段时间,但现在我看到它与 C# 或 Java 相比有多么不同。

这些天来,我认为 C++ 是最好的语言!是的,我可以理解有时我称之为“谷壳”的东西会更多一些(似乎是不必要的东西),但在真正认真使用该语言之后,我已经完全改变了主意。

我曾经一直有内存泄漏。我以前把我所有的代码都写到 .h 文件中,因为我讨厌代码的分离,我不明白他们为什么要这样做!而且我过去总是以愚蠢的循环包含依赖项而告终,并且堆积更多。我真的沉迷于 C# 或 Java,对我来说 C++ 是一个巨大的进步。这些天我明白了。我几乎从来没有内存泄漏,我喜欢接口和实现的分离,我不再有循环依赖的问题。

我也不会错过 finally 块。老实说,我的观点是,你所说的这些 C++ 程序员在 catch 块中编写重复的清理操作,在我看来,他们只是糟糕的 C++ 程序员。我的意思是,看起来这个线程中的任何其他 C++ 程序员都没有遇到你提到的任何问题。RAII 确实使 finally 变得多余,如果有的话,它的工作量就更少了。您编写了一个析构函数,然后您永远不必最终编写另一个析构函数!至少对于那种类型。

恕我直言,我认为你现在只是习惯了 Java,就像我以前一样。

于 2009-02-01T07:40:00.620 回答
11

C++ 的答案是 RAII:对象的析构函数将在超出范围时执行。无论是通过返回,通过异常还是其他方式。如果您在其他地方处理异常,您可以确保从被调用函数到您的处理程序的所有对象都将通过调用其析构函数来正确销毁。他们会为你清理。

阅读http://en.wikipedia.org/wiki/Resource_acquisition_is_initialization

于 2009-02-01T05:10:58.877 回答
10

No finally 没有被添加到 C++ 中,也不太可能被添加。

C++ 使用构造函数/析构函数的方式使得 finally 没有必要。
如果您使用 catch(...) 进行清理,那么您没有正确使用 C++。清理代码应该都在析构函数中。

尽管不需要使用它,但 C++ 确实有一个 std::exception。
强制开发人员从特定类派生以使用异常与 C++ 保持简单的理念背道而驰。这也是为什么我们不需要所有类都派生自 Object 的原因。

阅读:C++ 是否支持“终于”块?(我一直听到的这个“RAII”是什么?)

finally 的使用比析构函数更容易出错。
这是因为您强制对象的用户进行清理,而不是类的设计者/实现者。

于 2009-02-01T05:11:54.397 回答
9

好的,我必须在单独的答案帖子中添加对您提出的观点的答案:(如果您将其编辑到原始问题中会更方便,因此它不会出现在下面的底部答案。

如果所有清理总是在析构函数中完成,那么在 catch 块中就不需要任何清理代码 - 但是 C++ 有可以在其中完成清理操作的 catch 块。事实上,它有一个用于 catch(...) 的块,它只能执行清理操作(当然,不能获取任何异常信息来执行任何日志记录)。

catch 有一个完全不同的目的,作为一名 Java 程序员,您应该意识到这一点。finally 子句用于“无条件”清理操作。无论块如何退出,都必须这样做。Catch 用于条件清理。如果抛出这种类型的异常,我们需要执行一些额外的操作。

无论是否抛出异常,finally 块中的清理都将完成——这是当清理代码确实存在时人们总是希望发生的事情。

真的吗?如果我们希望这种类型总是发生这种情况(例如,我们总是想在完成后关闭数据库连接),那么我们为什么不定义一次呢?在类型本身?使数据库连接自行关闭,而不是在每次使用它时都尝试/最终?

这就是析构函数的重点。他们保证每种类型都能够在每次使用时自行进行清理,而无需调用者考虑。

从第一天开始,C++ 开发人员就不得不重复出现在代码流中的 catch 块中的清理操作,这些操作在成功退出 try 块时发生。Java 和 C# 程序员只需在 finally 块中执行一次。

不会。C++ 程序员从来没有被这个困扰过。C程序员有。C 程序员意识到 c++ 有类,然后称自己为 C++ 程序员。

我每天都用 C++ 和 C# 编程,我觉得我被 C# 荒谬的坚持所困扰,我using每次使用数据库连接或其他必须清理的东西时都必须提供 finally 子句(或块)。

C++ 让我一劳永逸地指定“只要我们完成了这种类型,它就应该执行这些操作”。我不会冒险忘记释放内存。我不会冒险忘记关闭文件句柄、套接字或数据库连接。因为我的记忆、我的句柄、套接字和数据库连接都是自己做的。

每次使用类型时都必须编写重复的清理代码,这怎么能更好?如果您需要包装类型,因为它本身没有析构函数,您有两个简单的选择:

  • 寻找提供此析构函数的适当 C++ 库(提示:Boost)
  • 使用 boost::shared_ptr 来包装它,并在运行时为它提供一个自定义函子,指定要完成的清理工作。

当您编写 Java EE 应用服务器 Glassfish、JBoss 等应用服务器软件时,您希望能够捕获并记录异常信息——而不是让它掉在地上。或者更糟糕的是陷入运行时并导致应用程序服务器突然退出。这就是为什么对于任何可能的异常都有一个总体基类是非常可取的。而 C++ 就是这样一个类。标准::异常。

自 CFront 时代以来,我一直在做 C++,在这十年的大部分时间里都在做 Java/C#。很明显,在处理基本相似的事情方面存在巨大的文化差距。

不,你从来没有做过 C++。你已经完成了 CFront 或 C 类。不是 C++。有很大的不同。不要说答案是蹩脚的,你可能会学到一些你认为你知道的语言。;)

于 2009-02-01T11:58:10.630 回答
5

清理函数本身是完全蹩脚的。他们的凝聚力很低,因为他们被期望执行一系列仅与它们发生时相关的活动。它们具有高耦合性,因为当实际执行某些操作的功能发生更改时,它们需要修改其内部结构。因此,它们很容易出错。

try...finally 构造是清理函数的框架。这是一种语言鼓励的方式来编写糟糕的代码。此外,由于它鼓励一遍又一遍地编写相同的清理代码,它破坏了 DRY 原则。

对于这些目的,C++ 方式要好得多。资源的清理代码在析构函数中只写一次。它与该资源的其余代码位于同一位置,因此具有良好的凝聚力。清理代码不必放入不相关的模块中,因此这减少了耦合。如果设计得当,它只写一次。

此外,C++ 方式更加统一。C++,加上智能指针,以同样的方式处理各种资源,而Java很好地处理内存,并提供了不充分的构造来释放其他资源。

C++ 有很多问题,但这不是其中之一。Java 在某些方面优于 C++,但这不是其中之一。

用一种方法来实现 RAII 而不是 try...finally,Java 会好得多。

于 2009-02-02T17:44:22.040 回答
4

为了避免必须为每个可释放资源定义一个包装类,您可能对 ScopeGuard ( http://www.ddj.com/cpp/184403758 ) 感兴趣,它允许您即时创建“清理程序”。

例如:

FILE* fp = SomeExternalFunction();
// Will automatically call fclose(fp) when going out of scope
ScopeGuard file_guard = MakeGuard(fclose, fp);
于 2009-02-02T14:29:03.023 回答
3

正确使用 finally 是多么困难的一个例子。

打开和关闭两个文件。
您要保证文件正确关闭的地方。
等待 GC 不是一个选项,因为这些文件可能会被重复使用。

在 C++ 中

void foo()
{
    std::ifstream    data("plop");
    std::ofstream    output("plep");

    // DO STUFF
    // Files closed auto-magically
}

在没有析构函数但有 finally 子句的语言中。

void foo()
{
    File            data("plop");
    File            output("plep");

    try
    {
        // DO STUFF
    }
    finally
    {
        // Must guarantee that both files are closed.
        try {data.close();}  catch(Throwable e){/*Ignore*/}
        try {output.close();}catch(Throwable e){/*Ignore*/}
    }
}

这是一个简单的例子,代码已经变得复杂了。在这里,我们只尝试编组 2 个简单的资源。但是随着需要管理的资源数量的增加和/或其复杂性的增加,finally 块的使用变得越来越难以在存在异常的情况下正确使用。

finally 的使用将正确使用的责任转移到对象的用户身上。通过使用 C++ 提供的构造函数/析构函数机制,您将正确使用的责任转移到类的设计者/实现者身上。这在继承上更安全,因为设计者只需要在类级别正确执行一次(而不是让不同的用户尝试以不同的方式正确执行)。

于 2009-02-02T16:51:25.687 回答
2

使用 C++11 及其lambda 表达式,我最近开始使用以下代码来模仿finally

class FinallyGuard {
private:
  std::function<void()> f_;
public:
  FinallyGuard(std::function<void()> f) : f_(f) { }
  ~FinallyGuard() { f_(); }
};

void foo() {
  // Code before the try/finally goes here
  { // Open a new scope for the try/finally
    FinallyGuard signalEndGuard([&]{
      // Code for the finally block goes here
    });
    // Code for the try block goes here
  } // End scope, will call destructor of FinallyGuard
  // Code after the try/finally goes here
}

TheFinallyGuard是一个使用可调用函数式参数构造的对象,最好是 lambda 表达式。它只会记住该函数,直到调用它的析构函数,当对象超出范围时,无论是由于正常的控制流还是由于异常处理期间的堆栈展开。在这两种情况下,析构函数都会调用函数,从而执行相关代码。

有点奇怪,你必须在代码块的代码finally 之前try编写代码,但除此之外,它实际上感觉很像真正的try/finally来自 Java。我想人们不应该在具有自己适当的析构函数的对象更合适的情况下滥用这一点,但在某些情况下,我认为上面的这种方法更合适。我在这个问题中讨论了一个这样的场景。

据我了解,std::function<void()>将使用一些指针间接和至少一个虚函数调用来执行其类型擦除,因此会有性能开销。不要在性能至关重要的紧密循环中使用此技术。在这些情况下,析构函数只做一件事的专用对象会更合适。

于 2013-03-05T13:21:36.277 回答
1

C++ 析构函数变得finally多余。您可以通过将清理代码从 finally 移动到相应的析构函数来获得相同的效果。

于 2009-02-01T06:49:22.610 回答
1

我认为您错过了catch (...)可以做什么的重点。

您在示例中说“唉,无法检查异常”。好吧,您没有关于异常类型的信息。你甚至不知道它是否是多态类型,所以即使你有某种无类型的引用,你甚至不能安全地尝试一个dynamic_cast.

如果你知道某些异常或异常层次结构,你可以用它来做一些事情,那么这里就是具有明确命名类型的 catch 块的地方。

catch (...)在 C++ 中并不常用。它可以用于必须保证不抛出或只抛出某些约定异常的地方。如果您正在使用catch (...)清理,那么很有可能您的代码在任何情况下都不是非常安全的异常安全。

正如其他答案中所提到的,如果您使用本地对象来管理资源(RAII),那么它通常会令人惊讶和启发您需要多少 catch 块 - 如果您不需要在本地执行任何异常操作 - 甚至try 块可能是多余的,因为您让异常流出到可以响应它们的客户端代码,同时仍然保证没有资源问题。

要回答您最初的问题,如果您需要在块的末尾运行一些代码,异常或无异常,那么配方就是。

class LocalFinallyReplacement {
    ~LocalFinallyReplacement() { /* Finally code goes here */ }
};
// ...
{ // some function...
    LocalFinallyReplacement lfr; // must be a named object

    // do something
}

请注意我们如何可以完全取消try,catchthrow

如果您的函数中有最初声明在 try 块之外的数据,您需要在“finally”块中访问这些数据,那么您可能需要将其添加到帮助程序类的构造函数中并将其存储到析构函数。然而,在这一点上,我会认真地重新考虑是否可以通过改变本地资源处理对象的设计来解决这个问题,因为这意味着设计中出现了一些错误。

于 2009-02-01T11:13:25.480 回答
1

不完全离题。

Java 中的锅炉电镀数据库资源清理

讽刺模式:Java 的成语是不是很精彩?

于 2009-02-02T18:57:24.030 回答
0

在这 15 年里,我在 C++ 中完成了大量的类设计和模板包装器设计,并在析构函数清理方面以 C++ 的方式完成了这些工作。但是,每个项目也总是涉及到 C 库的使用,这些库提供了打开、使用、关闭使用模型的资源。try/finally 意味着这样的资源可以在它需要的地方使用——以一种完全健壮的方式——并用它来完成。对这种情况进行编程的最不乏味的方法。可以处理在清理逻辑期间发生的所有其他状态,而不必在某些包装析构函数中限定范围。

我在 Windows 上完成了大部分 C++ 编码,因此在这种情况下总是可以求助于微软的 __try/__finally。(它们的结构化异常处理具有一些与异常交互的强大能力。)唉,看起来 C 语言从来没有批准过任何可移植的异常处理结构。

不过,这不是理想的解决方案,因为在 try 块中混合 C 和 C++ 代码并不简单,其中任何一种类型的异常都可能被抛出。添加到 C++ 的 finally 块对这些情况很有帮助,并且可以实现可移植性。

于 2009-02-01T17:01:21.783 回答
0

关于您的附录编辑,是的,正在考虑 C++0x 的闭包。它们可以与 RAII 范围的警卫一起使用,以提供易于使用的解决方案,请查看Pizer 的博客。它们也可以用来模仿try-finally,见这个答案;但这真的是个好主意吗?.

于 2009-02-02T19:41:42.910 回答
0

以为我会为此添加自己的解决方案-一种智能指针包装器,用于当您必须处理非 RAII 类型时。

像这样使用:

Finaliser< IMAPITable, Releaser > contentsTable;
// now contentsTable can be used as if it were of type IMAPITable*,
// but will be automatically released when it goes out of scope.

所以这里是 Finaliser 的实现:

/*  Finaliser
    Wrap an object and run some action on it when it runs out of scope.
    (A kind of 'finally.')
    * T: type of wrapped object.
    * R: type of a 'releaser' (class providing static void release( T* object )). */
template< class T, class R >
class Finaliser
{
private:
    T* object_;

public:
    explicit Finaliser( T* object = NULL )
    {
        object_ = object;
    }

    ~Finaliser() throw()
    {
        release();
    }

    Finaliser< T, R >& operator=( T* object )
    {
        if (object_ != object && object_ != NULL)
        {
            release();
        }
        object_ = object;

        return *this;
    }

    T* operator->() const
    {
        return object_;
    }

    T** operator&()
    {
        return &object_;
    }

    operator T*()
    {
        return object_;
    }

private:
    void release() throw()
    {
        R::release< T >( object_ );
    }
};

...这是发布者:

/*  Releaser
    Calls Release() on the object (for use with Finaliser). */
class Releaser
{
public:
    template< class T > static void release( T* object )
    {
        if (object != NULL)
        {
            object->Release();
        }
    }
};

我有几种像这样的释放器,包括一种用于 free() 和一种用于 CloseHandle()。

于 2009-03-13T09:42:19.643 回答