最终和析构函数之间的概念区别是什么?


12

首先,我很清楚为什么C ++中没有“最终”构造?但是关于另一个问题的冗长的评论讨论似乎需要一个单独的问题。

除了finally在C#和Java中每个作用域基本上只能存在一次(== 1)并且单个作用域可以具有多个(== n)C ++析构函数的问题之外,我认为它们本质上是同一件事。(存在一些技术差异。)

但是,另一个用户认为

...我试图说dtor本质上是(发布语义)的工具,而最终本质上是(提交语义)的工具。如果您不明白为什么,请考虑:为什么在finally块中相互抛出异常是合法的,以及为什么析构函数则不这样?(从某种意义上说,这是数据与控制的事情。析构函数用于释放数据,最终用于释放控制。它们是不同的;不幸的是C ++将它们绑在一起。)

有人可以清理吗?

Answers:


6
  • 交易(try
  • 错误输出/响应(catch
  • 外部错误(throw
  • 程序员错误(assert
  • 回滚(最接近的东西可能是本机支持它们的语言中的作用域防护)
  • 释放资源(析构函数)
  • 与事务无关的其他控制流(finally

与其他finally独立于事务的控制流相比,无法提供更好的描述。在事务和错误恢复心态中,它不一定直接映射到任何高级概念,尤其是在同时具有析构函数和的理论语言中finally

对我而言,最本质上缺乏的是一种语言功能,直接代表回滚外部副作用的概念。我能想到的最接近的事物是D之类的示波器防护,它几乎代表了这个概念。从控制流的角度来看,特定功能范围内的回滚将需要区分特殊路径与常规路径,同时在事务失败时隐式自动回滚由函数引起的任何副作用,但在事务成功时则不需要。如果我们succeeded在try块的末尾将布尔值设置为true,就很容易使用析构函数,以防止析构函数中的回滚逻辑。但这是一种相当round回的方式。

尽管这似乎并不能节省太多,但是副作用逆转是最难解决的问题之一(例如:使得编写异常安全的通用容器如此困难)。


4

从某种意义上说,它们与法拉利和大众运输车都可以用来压倒一品脱牛奶的商店一样,即使它们是为不同用途而设计的。

您可以在每个作用域中放置一个try / final构造,并在finally块中清除所有作用域定义的变量,以模拟C ++析构函数。从概念上讲,这就是C ++的作用-当变量超出范围时(即,在范围块的末尾),编译器会自动调用析构函数。您必须安排尝试/最后尝试,但是尝试是每个范围中的第一件事,最后是最后一件事。您还必须为每个对象定义一个标准,以具有一个专门命名的方法,该方法用于清理在finally块中调用的状态,尽管我想您可以保留语言提供的常规内存管理清理需要清空的对象。

尽管这样做并不好,尽管.NET引入IDispose作为手动管理的析构函数,并使用块作为尝试使手动管理稍微容易一些,但这仍然不是您在实践中想要做的事情。


4

从我的角度来看,主要区别在于c ++ 中的析构函数是 用于释放分配的资源的隐式机制(自动调用),而try ... 最终可以用作实现此目的的显式机制。

在c ++程序中,程序员负责释放分配的资源。这通常在类的析构函数中实现,并且在变量超出范围或调用delete时立即完成。

在c ++中,如果创建类的局部变量而不使用new实例的资源,则在发生异常时,析构函数会隐式释放实例的资源。

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

在java,c#和其他具有自动内存管理功能的系统中,系统垃圾回收器确定何时销毁类实例。

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

它没有隐式机制,因此您必须使用try finally对此进行显式编程

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}

在C ++中,为什么在发生异常的情况下仅对堆栈对象而不对堆对象自动调用析构函数?
乔治

@Giorgio因为堆资源位于不直接与调用堆栈绑定的内存空间中。例如,假设有一个2个线程的多线程应用程序,A并且B。如果抛出一个线程,则回滚A's事务不应破坏在中分配的资源B,例如-线程状态彼此独立,并且堆上的持久内存独立于两者。但是,通常在C ++中,堆内存仍与堆栈上的对象绑定。

@Giorgio例如,一个std::vector对象可能位于堆栈上,但指向堆上的内存-在这种情况下,向量对象(在堆栈上)及其内容(在堆上)都将在堆栈展开期间被释放。破坏堆栈上的向量将调用析构函数,该析构函数释放堆上的关联内存(并同样破坏那些堆元素)。通常,出于异常安全的考虑,大多数C ++对象都驻留在堆栈上,即使它们仅是指向堆上内存的句柄,也可以自动执行释放堆栈上的堆和堆栈内存的过程。

4

很高兴您将其发布为问题。:)

我试图说的是析构函数,并且finally在概念上是不同的:

  • 析构函数用于释放资源(数据
  • finally用于返回到呼叫者(控件

考虑一下这个假设的伪代码:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finally这里完全解决了控制问题,而不是资源管理问题。
出于多种原因,在析构函数中执行此操作是没有意义的:

  • 没有东西被“获得”或“创造”
  • 无法打印到日志文件不会导致资源泄漏,数据损坏等(假设此处的日志文件未反馈到其他地方的程序中)
  • logfile.print失败是合法的,而破坏(在概念上)不能失败

这是另一个示例,这次就像在Javascript中一样:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

同样,在以上示例中,没有要释放的资源。
实际上,该finally区块正在内部获取资源以实现其目标,这有可能失败。因此,使用析构函数(如果Javascript有一个)是没有意义的。

另一方面,在此示例中:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finally正在破坏资源b。这是一个数据问题。问题不在于将控制权干净地返回给调用方,而在于避免资源泄漏。
失败不是一种选择,并且应该(从概念上)永远不会发生。
每次发行b都必须与获取配对,因此使用RAII是有意义的。

换句话说,仅因为您可以使用其中一个模拟就不等于既是一个问题,又是同一问题,或者都不是两个问题的适当解决方案。


谢谢。我不同意,但是,嘿:-)我想我可以在接下来的几天中添加一个彻底的反对观点答案……
Martin Ba

2
finally最常用于释放(非内存)资源的事实对此有何影响?
Bart van Ingen Schenau 2015年

1
@BartvanIngenSchenau:我从不认为目前存在的任何语言都具有与我描述的语言相符的理念或实现。人们尚未发明出可能存在的一切。我只是说,将这两个概念分开是有价值的,因为它们是不同的想法并且具有不同的用例。为了满足您的好奇心,我相信D会同时兼顾。可能还有其他语言。我并不认为这是相关的,而且我也不在乎为什么Java这样的支持finally
user541686

1
我在JavaScript中遇到的一个实际示例是一些函数,该函数在进行一些冗长的操作(可能会引发异常)时将鼠标指针临时更改为沙漏,然后在finally子句中将其更改为正常。C ++世界观将引入一个类,该类管理对伪全局变量的分配的这种“资源” 。这有什么概念意义?但是析构函数是C ++要求执行块末尾代码执行的重锤。
dan04

1
@ dan04:非常感谢,这是一个完美的例子。我可以发誓我会遇到很多RAII没有意义的情况,但是我很难思考它们。
user541686

1

k3b的答案确实很好地表达了这一点:

c ++中的析构函数是用于释放分配的资源的隐式机制(自动调用),而try ...最终可以用作实现此目的的显式机制。

关于“资源”,我想引用乔恩·卡尔布Jon Kalb)的观点:RAII应该表示责任获取是初始化

无论如何,对于隐式还是显式,看来确实是这样:

  • d'tor是一种工具,用于定义在对象的生存期结束时(通常与范围结束时)隐式地发生什么操作。
  • 最终块是一种工具,用于明确定义在作用域结束时应进行的操作。
  • 另外,从技术上讲,总是允许您从决赛中掷出,但请参阅下文。

我认为概念部分就是这样,...


...现在有恕我直言一些有趣的细节:

除了负责在析构函数中运行某些代码的职责外,我也认为c'tor / d'tor在概念上不需要“获取”或“创建”任何东西。最后还要做的是:运行一些代码。

尽管finally块中的代码肯定会引发异常,但这对我来说还不足以区分它们在显式和隐式之间在概念上是不同的。

(此外,我完全不相信“好的”代码应该从最后扔掉-也许这是另一个完整的问题。)


您对我的Javascript示例有何看法?
user541686

关于您的其他论点:“我们是否真的想不管记录相同的事情?” 是的,这只是一个示例,您有点漏了点,是的,从来没有人禁止针对每种情况记录更具体的细节。这里的要点是,您当然不能断言永远不会出现想要记录两者共同的情况的情况。有些日志条目是通用的,有些是特定的。你们两个都想要。再一次,您将重点放在日志记录上,这完全是您遗漏的要点。激励10行示例很困难。请尽量不要错过重点。
user541686

您从未解决过这些问题
user541686

@Mehrdad-我没有解决您的javascript示例,因为它将带我另一页来讨论我的想法。(我尝试过,但花了我这么长时间,我说了一些连贯的内容,所以我略过了它:-)
Martin Ba

@Mehrdad-关于您的其他观点-看来我们必须同意不同意。我看到了您针对差异的目标,但我只是不确信它们在概念上有所不同:主要是因为我主要在阵营中,认为从头开始抛出是一个非常糟糕的主意(请注意:认为在您的observer示例中抛出一个真正的坏主意。)如果您想进一步讨论,请随意打开聊天。考虑您的论点当然很有趣。干杯。
马丁·巴
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.