长话短说 - 我正在编写一个编译器,并且达到了OOP功能,我面临着涉及处理析构函数的两难问题。 基本上我有两个选择:

  • 1 - 将所有析构函数放在程序中该点需要调用的对象上。 这个选项听起来像是性能友好且简单但会使代码膨胀,因为根据控制流程,某些析构函数可以多次复制。

  • 2 - 每个代码块的分区析构函数,带有标签和“意大利面条跳转”,仅通过所需的代码。 好处 - 没有析构函数将被复制,缺点 - 它将涉及非顺序执行和跳转,以及额外的隐藏变量和条件,这将需要例如确定执行是否留下块以继续在父级执行阻止或中断/继续/转到/返回,这也增加了它的复杂性。 额外的变量和检查很可能会占用这种方法所节省的空间,具体取决于对象的数量以及内部复杂的结构和控制流程。

而且我知道对这些问题的通常反应是“做两件事,分析和决定”,如果这是一项微不足道的任务,那就是我要做的事情,但编写一个功能齐全的编译器已经证明有点艰难,所以我更喜欢得到一些专家意见而不是而不是建造两座桥梁,看看哪座桥梁做得更好,又烧掉另一座桥梁。

我把c ++放在标签中,因为这是我正在使用的语言,我对它和RAII范例有点熟悉,这也是我的编译器正在建模的内容。

===============>>#1 票数:4

在大多数情况下,析构函数调用可以与普通函数调用相同的方式处理。

较小的部分是处理EH。 我注意到MSC在“普通”代码中生成内联析构函数调用的混合,并且对于x86-64,创建单独的清理代码,其本身可能有也可能没有析构函数逻辑的副本。

IMO,最简单的解决方案是始终将非平凡的析构函数称为普通函数。

如果看起来可能出现优化,那么就像上面的其他内容一样处理上述调用:它是否适合其他所有内容? 这样做会占用图像中太多的空间吗? 等等..

前端可以在其输出AST中的每个可操作块的末尾向非平凡析构函数插入“调用”。

后端可以处理诸如普通函数调用之类的东西,将它们连接在一起,在某处创建一个大块o-析构函数调用逻辑并跳转到那个等等......

将函数链接到相同的逻辑似乎很常见。 例如,MSC倾向于将所有普通函数链接到相同的实现,析构函数或其他,优化与否。

这主要来自经验。 像往常一样,YMMV。

还有一件事:

EH清理逻辑往往像跳转表一样工作:对于给定的函数,您可以跳转到单个析构函数调用列表,具体取决于抛出异常的位置(如果适用)。

===============>>#2 票数:2

我不知道商业编译器如何提出代码,但假设我们忽略了异常[1],我采取的方法是调用析构函数,而不是内联它。 每个析构函数都包含该对象的完整析构函数。 使用循环来处理数组的析构函数。

内联调用是一种优化,除非你“知道它得到回报”(代码大小与速度),否则你不应该这样做。

你将需要处理“封闭块中的破坏”,但假设你没有跳出块,那应该很容易。 跳出块(例如返回,中断等)意味着你必须跳转到一段代码来清理你所在的块。

[1]商业编译器具有基于“抛出异常的位置”的特殊表,以及为执行该清理而生成的一段代码 - 通常通过在每个清理块中具有多个跳转标签来对许多异常点重用相同的清理。

===============>>#3 票数:2

编译器使用两种方法的混合。 MSVC使用内联析构函数调用正常的代码流,并以相反的顺序清理代码块以用于早期返回和异常。 在正常流程中,它使用单个隐藏的本地整数来跟踪到目前为止的构造函数进度,因此它知道在早期返回时跳转到何处。 单个整数就足够了,因为范围总是形成一个树(而不是说为已经成功构建或未成功构建的每个类使用位掩码)。 例如,以下相当短的代码使用具有非平凡析构函数的类和一些随机返回在整个...

    ...
    if (randomBool()) return;
    Foo a;
    if (randomBool()) return;
    Foo b;
    if (randomBool()) return;

    {
        Foo c;
        if (randomBool()) return;
    }

    {
        Foo d;
        if (randomBool()) return;
    }
    ...

...可以在x86上扩展为如下所示的伪代码,其中构造函数进度在每个构造函数调用之后立即递增(有时通过多于一个到下一个唯一值)并在每个构造函数紧接之前递减(或“弹出”到更早的值)析构函数。 请注意,具有普通析构函数的类不会影响此值。

    ...
    save previous exception handler // for x86, not 64-bit table based handling
    preallocate stack space for locals
    set new exception handler address to ExceptionCleanup
    set constructor progress = 0
    if randomBool(), goto Cleanup0
    Foo a;
    set constructor progress = 1 // Advance 1
    if randomBool(), goto Cleanup1
    Foo b;
    set constructor progress = 2 // And once more
    if randomBool(), goto Cleanup2

    {
        Foo c;
        set constructor progress = 3
        if randomBool(), goto Cleanup3
        set constructor progress = 2 // Pop to 2 again
        c.~Foo();
    }

    {
        Foo d;
        set constructor progress = 4 // Increment 2 to 4, not 3 again
        if randomBool(), goto Cleanup4
        set constructor progress = 2 // Pop to 2 again
        d.~Foo();
    }

// alternate Cleanup2
    set constructor progress = 1
    b.~Foo();
// alternate Cleanup1
    set constructor progress = 0
    a.~Foo();

Cleanup0:
    restore previous exception handler
    wipe stack space for locals
    return;

ExceptionCleanup:
    switch (constructor progress)
    {
    case 0: goto Cleanup0; // nothing to destroy
    case 1: goto Cleanup1;
    case 2: goto Cleanup2;
    case 3: goto Cleanup3;
    case 4: goto Cleanup4;
    }
    // admitting ignorance here, as I don't know how the exception
    // is propagated upward, and whether the exact same cleanup
    // blocks are shared for both early returns and exceptions.

Cleanup4:
    set constructor progress = 2
    d.~Foo();
    goto Cleanup2;
Cleanup3:
    set constructor progress = 2
    c.~Foo();
    // fall through to Cleanup2;
Cleanup2:
    set constructor progress = 1
    b.~Foo();
Cleanup1:
    set constructor progress = 0
    a.~Foo();
    goto Cleanup0;
    // or it may instead return directly here

编译器当然可以重新排列这些块,无论如何它认为更有效,而不是将所有清理结束。 早期的返回可能会跳转到函数末尾的备用Cleanup1 / 2。 在64位MSVC代码上,异常通过表来处理,这些表将异常发生的指令指针映射到相应的代码清理块。

===============>>#4 票数:1

优化编译器正在转换已编译源代码的内部表示。

它通常构建基本块的定向(通常是循环)图。 构建此控制流图时,它会将调用添加到析构函数中。

对于GCC (它是一个免费的软件编译器 - 以及Clang / LLVM - 所以你可以研究它的源代码),你可能会尝试用-fdump-tree-all编译一些简单的C ++测试用例代码,然后看看它是在简化时完成的。 顺便说一句,您可以使用MELT自定义g++来探索其内部表示。

顺便说一句,我不认为你如何处理析构函数是重要的(请注意,在C ++中他们被隐式调用在语法上定义的地方,比如}其定义范围)。 这种编译器的大部分工作都在优化(然后,处理析构函数不是很相关;它们几乎就像其他程序一样)。

  ask by translate from so

未解决问题?本站智能推荐:

4回复

为什么不为基类强制执行(C ++)虚拟析构函数

默认情况下,析构函数不是虚拟的,在不需要它时不会受到伤害,这很好。 但是在基类派生类场景的情况下, 是否有没有虚拟析构函数的用例 ? 如果没有可能(有意义的话),如果一个类派生自一个定义了公共非虚拟析构函数(或没有析构函数)的基类,编译器就会抱怨。 而不只是警告它。
4回复

调用析构函数的顺序和要点

可以说我有两个本地对象。 当函数返回时,是否保证哪一个将首先超出范围? 例如: 我有一个这样的课: 这是一个非常常见的技巧,用于在超出范围时自动释放互斥锁。 但是如果我在范围内需要两个互斥量呢? 这真的不会造成任何僵局。 但是可能存在释放资源的顺序对用户有用的实
1回复

析构函数的模板参数推导

调用类的析构函数时,编译器是否应推导出模板参数? 以下代码: 在gcc(g ++ 4.3.4)上可以正常编译,但在XLC ++上使用 可以从符合标准的编译器中获得两种行为中的哪一种?
1回复

为什么C ++偏向于析构函数的异常?

当我执行此代码时,它不会进入catch块。 并给出以下例外, 但是,当我不加修改地运行此代码时,它就会捕获块。 输出: 上面的代码都抛出2个异常,那么为什么在析构函数的情况下偏向于编译器?
2回复

实现析构函数的功能?

我对类的析构函数的实现有疑问。 我知道正确的方法是使用〜运算符,但请看以下代码: 现在让我们说主要功能定义如下: 在main中对func()进行函数调用时,这是否以与析构函数完全相同的方式工作? 在任何类似的设置中,析构函数和此函数有什么区别?
4回复

如何处理界面中的析构函数

当我用C ++编写接口类时,我选择以下两个选项之一 要么 第一个版本写得更短 第二个是有吸引力的,因为界面的所有功能都是纯虚拟的 有什么理由我应该选择一种或另一种方法(或者可能是第三种方法)? 谢谢
3回复

什么法律代码可以触发C4523“指定多个析构函数”的Visual C ++警告?

根据MSDN,Visual C ++可以发出C4523警告 'class':指定多个析构函数 。 这种情况怎么可能呢? 我尝试了以下方法: 产生析构函数必须有'void'参数列表错误和C4523警告以及以下内容 它产生成员函数已经定义或声明的错误和以下内容 产生析
3回复

关于我不理解的类对象的编译器行为

我是菜鸟,仍在学习c ++语言。 事情是,从书中进行练习,我遇到了我不理解的编译器行为。 头文件。 类的实现。 主文件。 您可能已经猜到,Stock是类,我创建了非默认的构造函数和析构函数以显示消息以查看它们何时“起作用”。 这是程序执行的输出:
3回复

析构函数如何执行?

我在Destructor上进行过实践,但是在编译该程序时,我不知道为什么输出不如我所想。 输出为: 我以为是这样的:
3回复

C ++析构函数

考虑这种情况:我需要为某些设置创建一个ui。 由于数据和ui在理论上应该分开,因此我定义了一个单独的类来处理配置数据。 我的问题是如何在设置类中实例化数据类。 一种方法是在调用方对象中创建数据类,我的意思是调用设置菜单类的对象。 我的问题所涉及的另一种方法是在settings类中