【问题标题】:Are Fortran's "final" subroutines reliable enough for practical use?Fortran 的“最终”子程序对于实际使用是否足够可靠?
【发布时间】:2020-05-16 00:54:13
【问题描述】:

现代 Fortran 包含各种面向对象的思想,包括通过 FINAL 关键字的“析构函数”概念。

MODULE mobject
  TYPE :: tobject
    ! Data declarations
  CONTAINS
    FINAL :: finalize
  END TYPE
CONTAINS
  SUBROUTINE finalize(object)
    TYPE(tobject) object
    ...
  END SUBROUTINE
END MODULE

但是,这个功能可靠吗?值得注意的是,我注意到关于何时以及是否会调用它的不一致之处,英特尔 Fortran 19 和 GFortan 7、8 之间存在重大差异:

  • GFortran 无法销毁存储在数组中的对象。
  • 英特尔 Fortran:
    • 在分配时执行虚假和可能多余的破坏,甚至可能在包含垃圾数据的内存上执行,并且
    • 在从函数返回时对析构函数执行虚假调用。

我注意到 gfortran-7.4.0 和 gfortran-8.2.1.2 之间没有区别。

这些不一致对我来说对析构函数的实际可用性提出了一些问题。其中任何一种行为是否完全符合标准?这个标准不清楚吗?标准是否可能包含导致不直观行为的条款?

详细分析(代码见下)

  • PROGRAM 块。 Gfortran 不会为 PROGRAM 块中声明的实例调用析构函数,而 Ifort 会调用(参见示例中的run1)。

  • 标量对象。对于声明为标量的实例,如果变量已经看到任何形式的初始化,Gfortran 和 IFort 都将调用析构函数。然而,Intel Fortran 在分配函数返回值时,也会调用它

    • 在堆栈上未初始化的对象上,然后用函数中的数据覆盖它,并且
    • 似乎在 newObject 函数的末尾。

    然而,这可以通过明确检查是否 在执行任何清理之前初始化对象。

这意味着程序员必须明确检查实例是否已初始化。

  • 数组中的对象。如果对象包含在数组中,并且数组超出范围,

    • Gfortran 不会调用析构函数。
    • 英特尔 Fortran 可能会调用析构函数,具体取决于给定数组成员的初始化方式。
    • 数组是否声明allocatable没有区别。
  • 通过赋值初始化的可分配数组。 当使用现代功能时,分配给可分配数组意味着分配,这同样适用,只是没有 IntelFortran 可以调用的未初始化实例析构函数。

  • 来自函数的可分配/指针。

    • GFortran 不会在函数结束时调用析构函数,将allocatable 对象或pointer 返回给对象,而是在客户端代码中显式或通过以下方式释放值时调用它超出allocatables 的范围。这正是我的预期。
    • 英特尔 Fortran 调用在某些其他情况下:
      • 当对象声明为 allocatable,但不是 pointer 时,英特尔 Fortran 在退出函数时调用函数的本地值的析构函数。
      • 在函数内部使用隐式分配 (var = newObject(...)) 初始化对象时,或者在 pointer 变体的情况下,使用显式分配 (allocate(var); var = newObject(...)) 时,在未初始化的内存上调用析构函数,在 @ 中可见来自%name 的987654334@ 和run6MovePtr 包含垃圾数据。这可以通过使用 allocate(var); call var%init(...) 模式来解决。

测试代码

!! -- Makefile ---------------------------------------------------
!! Runs the code with various compilers.

SHELL = bash
FC = NO_COMPILER_SPECIFIED
COMPILERS = gfortran-7 gfortran-8 ifort
PR = @echo$(n)pr -m -t -w 100

define n


endef

all: 
    rm -rf *.mod *.bin
    $(foreach FC, $(COMPILERS), $(n)\
      rm -rf *.mod && \
      $(FC) destructor.f90 -o $(FC).bin && \
      chmod +x $(FC).bin)
    $(PR) $(foreach FC, $(COMPILERS), <(head -1 <($(FC) --version)))
    $(info)
    $(foreach N,0 1 2 3 4 5 6,$(n) \
      $(PR) $(foreach FC, $(COMPILERS), <(./$(FC).bin $(N))))



!! -- destructor.f90 ---------------------------------------------

module mobject
  implicit none
  private
  public tobject, newObject

  type :: tobject
     character(32) :: name = "<undef>"
   contains
     procedure :: init
     final :: finalize
  end type tobject

contains

  subroutine init(object, name)
    class(tobject), intent(inout) :: object
    character(*), intent(in) :: name
    print *, "+ ", name
    object%name = name
  end subroutine init

  function newObject(name)
    type(tobject) :: newObject
    character(*), intent(in) :: name
    call new%init(name)
  end function newObject

  subroutine finalize(object)
    type(tobject) :: object
    print *, "- ", object%name
  end subroutine finalize

end module mobject



module mrun
  use mobject
  implicit none
contains

  subroutine run1()
    type(tobject) :: o1_uninit, o2_field_assigned, o3_tobject, o4_new, o6_init
    type(tobject), allocatable :: o5_new_alloc, o7_init_alloc
    print *, ">>>>> run1"
    o2_field_assigned%name = "o2_field_assigned"
    o3_tobject = tobject("o3_tobject")
    o4_new = newObject("o4_new")
    o5_new_alloc = newObject("o5_new_alloc")
    call o6_init%init("o6_init")
    allocate(o7_init_alloc)
    call o7_init_alloc%init("o7_init_alloc")
    print *, "<<<<< run1"
  end subroutine run1

  subroutine run2Array()
    type(tobject) :: objects(4)
    print *, ">>>>> run2Array"
    objects(1)%name = "objects(1)_uninit"
    objects(2) = tobject("objects(2)_tobject")
    objects(3) = newObject("objects(3)_new")
    call objects(4)%init("objects(4)_init")
    print *, "<<<<< run2Array"
  end subroutine run2Array

  subroutine run3AllocArr()
    type(tobject), allocatable :: objects(:)
    print *, ">>>>> run3AllocArr"
    allocate(objects(4))
    objects(1)%name = "objects(1)_uninit"
    objects(2) = tobject("objects(2)_tobject")
    objects(3) = newObject("objects(3)_new")
    call objects(4)%init("objects(4)_init")
    print *, "<<<<< run3AllocArr"
  end subroutine run3AllocArr

  subroutine run4AllocArrAssgn()
    type(tobject), allocatable :: objects(:)
    print *, ">>>>> run4AllocArrAssgn"
    objects = [ &
         tobject("objects(1)_tobject"), &
         newObject("objects(2)_new") ]
    print *, "<<<<< run4AllocArrAssgn"
  end subroutine run4AllocArrAssgn

  subroutine run5MoveAlloc()
    type(tobject), allocatable :: o_alloc
    print *, ">>>>> run5MoveAlloc"
    o_alloc = getAlloc()
    print *, "<<<<< run5MoveAlloc"
  end subroutine run5MoveAlloc

  function getAlloc() result(object)
    type(tobject), allocatable :: object
    print *, ">>>>> getAlloc"
    allocate(object)
    object = newObject("o_alloc")
    print *, "<<<<< getAlloc"
  end function getAlloc

  subroutine run6MovePtr()
    type(tobject), pointer :: o_pointer
    print *, ">>>>> run6MovePtr"
    o_pointer => getPtr()
    deallocate(o_pointer)
    print *, "<<<<< run6MovePtr"
  end subroutine run6MovePtr

  function getPtr() result(object)
    type(tobject), pointer :: object
    print *, ">>>>> getPtr"
    allocate(object)
    object = newObject("o_pointer")
    print *, "<<<<< getPtr"
  end function getPtr

end module mrun



program main
  use mobject
  use mrun
  implicit none
  type(tobject) :: object
  character(1) :: argument

  print *, ">>>>> main"
  call get_command_argument(1, argument)
  select case (argument)
  case("1")
     call run1()
  case("2")
     call run2Array()
  case("3")
     call run3AllocArr()
  case("4")
     call run4AllocArrAssgn()
  case("5")
     call run5MoveAlloc()
  case("6")
     call run6MovePtr()
  case("0")
     print *, "####################";
     print *, ">>>>> runDirectlyInMain"
     object = newObject("object_in_main")
     print *, "<<<<< runDirectlyInMain"
  case default
     print *, "Incorrect commandline argument"
  end select
  print *, "<<<<< main"
end program main

测试代码的输出

>> make
rm -rf *.mod *.bin
rm -rf *.mod && gfortran-7 destructor.f90 -o gfortran-7.bin && chmod +x gfortran-7.bin  
rm -rf *.mod && gfortran-8 destructor.f90 -o gfortran-8.bin && chmod +x gfortran-8.bin  
rm -rf *.mod && ifort destructor.f90 -o ifort.bin && chmod +x ifort.bin

pr -m -t -w 100  <(head -1 <(gfortran-7 --version))  <(head -1 <(gfortran-8 --version))  <(head -1 <(ifort --version))
GNU Fortran (SUSE Linux) 7.4.0   GNU Fortran (SUSE Linux) 8.2.1 2 ifort (IFORT) 19.0.4.243 2019041

pr -m -t -w 100  <(./gfortran-7.bin 0)  <(./gfortran-8.bin 0)  <(./ifort.bin 0) 
 >>>>> main                       >>>>> main                       >>>>> main
 ####################             ####################             ####################
 >>>>> runDirectlyInMain          >>>>> runDirectlyInMain          >>>>> runDirectlyInMain
 + object_in_main                 + object_in_main                 + object_in_main
 <<<<< runDirectlyInMain          <<<<< runDirectlyInMain          - <undef>
 <<<<< main                       <<<<< main                       - object_in_main
                                                                   <<<<< runDirectlyInMain
                                                                   <<<<< main

pr -m -t -w 100  <(./gfortran-7.bin 1)  <(./gfortran-8.bin 1)  <(./ifort.bin 1) 
 >>>>> main                       >>>>> main                       >>>>> main
 >>>>> run1                       >>>>> run1                       >>>>> run1
 + o4_new                         + o4_new                         - <undef>
 + o5_new_alloc                   + o5_new_alloc                   + o4_new
 + o6_init                        + o6_init                        - <undef>
 + o7_init_alloc                  + o7_init_alloc                  - o4_new
 <<<<< run1                       <<<<< run1                       + o5_new_alloc
 - o7_init_alloc                  - o7_init_alloc                  - o5_new_alloc
 - o6_init                        - o6_init                        + o6_init
 - o5_new_alloc                   - o5_new_alloc                   + o7_init_alloc
 - o4_new                         - o4_new                         <<<<< run1
 - o3_tobject                     - o3_tobject                     - <undef>
 - o2_field_assigned              - o2_field_assigned              - o2_field_assigned
 <<<<< main                       <<<<< main                       - o3_tobject
                                                                   - o4_new
                                                                   - o6_init
                                                                   - o5_new_alloc
                                                                   - o7_init_alloc
                                                                   <<<<< main

pr -m -t -w 100  <(./gfortran-7.bin 2)  <(./gfortran-8.bin 2)  <(./ifort.bin 2) 
 >>>>> main                       >>>>> main                       >>>>> main
 >>>>> run2Array                  >>>>> run2Array                  >>>>> run2Array
 + objects(3)_new                 + objects(3)_new                 - <undef>
 + objects(4)_init                + objects(4)_init                + objects(3)_new
 <<<<< run2Array                  <<<<< run2Array                  - <undef>
 <<<<< main                       <<<<< main                       - objects(3)_new
                                                                   + objects(4)_init
                                                                   <<<<< run2Array
                                                                   <<<<< main

pr -m -t -w 100  <(./gfortran-7.bin 3)  <(./gfortran-8.bin 3)  <(./ifort.bin 3) 
 >>>>> main                       >>>>> main                       >>>>> main
 >>>>> run3AllocArr               >>>>> run3AllocArr               >>>>> run3AllocArr
 + objects(3)_new                 + objects(3)_new                 - <undef>
 + objects(4)_init                + objects(4)_init                + objects(3)_new
 <<<<< run3AllocArr               <<<<< run3AllocArr               - <undef>
 <<<<< main                       <<<<< main                       - objects(3)_new
                                                                   + objects(4)_init
                                                                   <<<<< run3AllocArr
                                                                   <<<<< main

pr -m -t -w 100  <(./gfortran-7.bin 4)  <(./gfortran-8.bin 4)  <(./ifort.bin 4) 
 >>>>> main                       >>>>> main                       >>>>> main
 >>>>> run4AllocArrAssgn          >>>>> run4AllocArrAssgn          >>>>> run4AllocArrAssgn
 + objects(2)_new                 + objects(2)_new                 + objects(2)_new
 <<<<< run4AllocArrAssgn          <<<<< run4AllocArrAssgn          - objects(2)_new
 <<<<< main                       <<<<< main                       <<<<< run4AllocArrAssgn
                                                                   <<<<< main

pr -m -t -w 100  <(./gfortran-7.bin 5)  <(./gfortran-8.bin 5)  <(./ifort.bin 5) 
 >>>>> main                       >>>>> main                       >>>>> main
 >>>>> run5MoveAlloc              >>>>> run5MoveAlloc              >>>>> run5MoveAlloc
 >>>>> getAlloc                   >>>>> getAlloc                   >>>>> getAlloc
 + o_alloc                        + o_alloc                        + o_alloc
 <<<<< getAlloc                   <<<<< getAlloc                   - `4�\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
 <<<<< run5MoveAlloc              <<<<< run5MoveAlloc              - o_alloc
 - o_alloc                        - o_alloc                        <<<<< getAlloc
 <<<<< main                       <<<<< main                       - o_alloc
                                                                   <<<<< run5MoveAlloc
                                                                   - o_alloc
                                                                   <<<<< main

pr -m -t -w 100  <(./gfortran-7.bin 6)  <(./gfortran-8.bin 6)  <(./ifort.bin 6)
 >>>>> main                       >>>>> main                       >>>>> main
 >>>>> run6MovePtr                >>>>> run6MovePtr                >>>>> run6MovePtr
 >>>>> getPtr                     >>>>> getPtr                     >>>>> getPtr
 + o_pointer                      + o_pointer                      + o_pointer
 <<<<< getPtr                     <<<<< getPtr                     - `��\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
 - o_pointer                      - o_pointer                      - o_pointer
 <<<<< run6MovePtr                <<<<< run6MovePtr                <<<<< getPtr
 <<<<< main                       <<<<< main                       - o_pointer
                                                                   <<<<< run6MovePtr
                                                                   <<<<< main

【问题讨论】:

    标签: fortran gfortran intel-fortran fortran2003


    【解决方案1】:

    TLDR:Gfortran 中存在已知的未解决问题。英特尔声称全力支持。一些编译器声称不支持。


    关于可靠性和可用性的问题一般来说是相当主观的,因为你必须考虑许多对你来说独特的点(你需要支持多个编译器吗?你需要支持他们的旧版本吗?到底是哪些?如何关键是如果某个实体没有最终确定?)。

    您提出的一些主张如果没有实际的代码示例就很难回答,并且可能是单独的完整问题和答案的主题。 Gfortran 在此错误报告https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37336 中发布了 Fortran 2003 和 2008 功能的当前实施状态(链接指向一个元错误,该错误指向在 bugzilla 中跟踪的几个单独问题)。众所周知,该功能尚未完成,并且存在未解决的问题。最值得注意的是(至少对我而言),函数结果尚未最终确定。其他编译器的状态概述(简化为 Y/N/partitally)位于 http://fortranwiki.org/fortran/show/Fortran+2003+status,并且曾经在 Fortran 论坛文章中定期更新。

    我不能谈论那些所谓的英特尔 Fortran 虚假最终确定。如果您在编译器中发现了错误,您应该向供应商提交错误报告。英特尔通常反应迅速。

    不过,可以回答一些个别问题。您可能会找到关于它们的单独 Q/As。但是:

    • Gfortran 不会为在 PROGRAM 块中声明的实例调用析构函数,而 Ifort 会(参见示例中的 run1)。

      • 在主程序中声明的变量根据标准隐式获取save 属性。编译器不应该生成任何自动终结。
    • 然而,英特尔 Fortran 在分配函数返回值时,也会调用它

      • 正如 Gfortran bugzilla 中所指出的,gfortran 尚未最终确定函数结果变量。
    • 这意味着程序员必须明确检查实例是否已初始化。

      • 恐怕Fortran标准中没有这个概念。我不知道“如果变量已经看到任何形式的初始化”可能意味着什么。请注意,初始化函数与其他函数一样
    • 使用现代功能时,分配给可分配数组意味着分配,这同样适用,只是没有 IntelFortran 可以调用析构函数的未初始化实例。

      • 不确定这实际上意味着什么。 Fortran 中没有这样的“初始化”。也许函数又出结果了?
    • 来自函数的可分配/指针。

      • 正如多次指出的那样,函数结果在当前版本的 Gfortran 中没有正确完成。

    如果您想详细回答任何问题,您确实必须提出一个具体问题。这个太宽泛了。此站点的帮助/说明包含“请编辑问题以将其限制为具有足够详细信息的特定问题,以确定适当的答案。避免一次提出多个不同的问题。请参阅 [求]帮助澄清这个问题。”

    【讨论】:

    • 关于主观性,我不太明白这个问题。一个没有在崩溃前调用或多次调用的析构函数应该总是引发一个危险信号。我能想到的最简单的情况是一个对象作为缓冲写入文件的代理;它将保存一个缓冲区和文件句柄(可能通过 fortran 中的一个单元);意外的破坏会导致崩溃,丢失数据的破坏力不足。通过询问多个实现,可移植性的相关性应该相当清楚。
    • @kdb 当然这是一个危险信号。但实用性是主观的。我已经写了为什么。当不应该有最终确定时,就不应该有最终确定。但我不知道有任何此类情况。如果是,请向您的供应商报告。如果你有一些例子,恐怕它会在你的问题的太多点和有太多选项的代码中丢失。您可能想就这一点提出具体问题
    • @kdb 转载,我已经列出了状态。我们还能说什么?你必须得出你的具体结论。我们真的不能说它目前是否对您来说足够便携。你必须自己决定。我们只能列出未解决的问题,并可能为其他编译器链接fortranwiki.org/fortran/show/Fortran+2003+status 中的表格。但是我们不能说它是否足够便携。我们只能说有些编译器确实支持它,有些不支持,有些部分支持。我个人没有在我的主要代码中使用终结。你必须自己决定。
    • @kdb,我同意 Vladimir F:如果您的代码中的 ifort 在不应该完成最终确定的情况下,那就是一个问题。但是,如果您的最终子例程引用了一个(可能)未定义的组件(例如在print *, "- ", object%name 中),那么这是您的代码的问题,而不是最终确定的 ifort/可移植性。
    • @fracescalus 根据标准的语义是否允许从函数返回对象而不调用析构函数?我会例外,至少可以在不调用析构函数的情况下返回 pointer。像这样返回 allocatable 会更理想,但这需要移动语义,据我所知,这不在标准中。
    猜你喜欢
    • 1970-01-01
    • 2011-03-29
    • 2011-03-25
    • 2014-04-24
    • 2013-08-12
    • 1970-01-01
    • 2012-01-23
    • 2012-09-17
    • 2011-04-11
    相关资源
    最近更新 更多