通过指针访问类型后派生类型中 Fortran 字符串的奇怪行为

Odd behavior of fortran strings in a derived type after accessing the type by a pointer

[本文末尾的工作示例 post!]

我正在尝试编写一个简单的模块来处理算术运算中的物理单位。我的目标是从主要单位中创建派生单位。

在下面的代码中可以看到,我有一个派生类型,即unit_t,它存储了一个字符串,代表单位本身,单位的幂,转换系数(将其转换为SI ), 一个逻辑变量来显示单元是否被克隆以及 nextprev 指向下一个或前一个单元的指针(如果我们有单元的组合,例如 kg * m / s**2 ,所以基本上它是一个将不同单元相互连接的链表)。

我有一个名为 unit_clone 的函数来克隆一个主要单元。 unit_int_pow 函数重载求幂运算符 (**),它只是克隆给定的主要单元并更新其指数。 units_mul 函数重载了乘法运算符 (*)。这个函数首先检查两个给定的单元是否被克隆(如果没有,它克隆它们)然后使用 nextprev 指针连接它们。

这是我的代码(你应该可以用gfortran编译它)

module units

  implicit none

  type unit_t
    character(len=16) :: symb
    integer :: pow
    real :: conv
    logical :: cloned
    type(unit_t), pointer :: next => null(), prev => null()
  end type unit_t


  ! definitions
  type(unit_t), target :: m = unit_t("m", 1, 1.d0, .false.)
  type(unit_t), target :: km = unit_t("km", 1, 1.d3, .false.)

  type(unit_t), target :: kg = unit_t("kg", 1, 1.d0, .false.)

  type(unit_t), target :: s = unit_t("s", 1, 1.d0, .false.)

  interface operator (**)
    procedure unit_int_pow
  end interface operator (**)


  interface operator (*)
    procedure units_mul
  end interface operator (*)

contains

  !> Cloning a given node (unit)
  function unit_clone(u) result (clone)
    implicit none

    type(unit_t), intent(in) :: u
    type(unit_t), allocatable, target :: clone

    allocate(clone)

    clone%symb = u%symb
    clone%conv = u%conv
    clone%pow = u%pow
    clone%cloned = .true.
    clone%next => u%next
    clone%prev => u%prev
  end function unit_clone


  !> integer powers
  function unit_int_pow(u1, p) result(u)
    implicit none

    type(unit_t), intent(in) :: u1
    integer, intent(in) :: p

    type(unit_t), allocatable, target :: u

    u = unit_clone(u1)
    u%pow = u%pow * p
  end function unit_int_pow


  !> multiplication
  function units_mul (u1, u2) result (u1c)
    implicit none

    type(unit_t), intent(in) :: u1, u2
    type(unit_t), allocatable, target :: u1c, u2c

    if ( u1%cloned ) then
      u1c = u1
    else
      u1c = unit_clone(u1)
    end if

    if ( u2%cloned ) then
      u2c = u2
    else
      u2c = unit_clone(u2)
    end if

    u2c%prev => u1c
    u1c%next => u2c
  end function units_mul
end module units

program test
  use units

  implicit none

  type(unit_t) :: u

  u = kg**2 * m

  print *, u%symb, "^", u%pow, " [expected: kg^2]"
  print *, u%next%symb, "^", u%next%pow, " [expected: m^1]"
  print *, u%next%prev%symb, "^", u%next%prev%pow, " [expected: kg^2]"
end program test

问题是,我得到以下输出:

kg            ^           2  [expected: kg^2]
 �ȷ2�U        ^           1  [expected: m^1]
 �ȷ2�U        ^           2  [expected: kg^2]

显然,在访问 nextnext%prev 单元(基本上是这个短链表的头部)之后,代码输出随机字符而不是 symbs。如果我更改派生类型中变量的顺序 unit_t,例如如果我将 symb 放在派生类型的末尾,我将得到正确的 symbs,但是这时间错误 pows.

知道这种奇怪行为的罪魁祸首是什么吗?


根据下面 Rudrigo 的评论,我重写了代码,现在可以正常工作了。仅供参考,工作代码如下(如果您有进一步的建议或修改,请告诉我,Nombre respository

module units

  implicit none

  type unit_t
    character(len=16) :: symb
    real :: conv
    real :: pow = 1.d0
    logical :: cloned = .false.
    type(unit_t), pointer :: next => null(), prev => null()
  end type unit_t


  ! units definitions
  type(unit_t), target :: m = unit_t("m", 1.d0)
  type(unit_t), target :: km = unit_t("km", 1.d3)

  type(unit_t), target :: kg = unit_t("kg", 1.d0)

  type(unit_t), target :: s = unit_t("s", 1.d0)


  interface operator (**)
    procedure unit_int_pow
  end interface operator (**)


  interface operator (*)
    procedure units_mul
  end interface operator (*)

contains

  !> Cloning a given node (unit)
  function unit_clone(u) result (clone)
    implicit none

    type(unit_t), intent(in) :: u
    type(unit_t), pointer :: clone

    allocate(clone)

    clone%symb = trim(u%symb)
    clone%conv = u%conv
    clone%pow = u%pow
    clone%cloned = .true.
    clone%next => u%next
    clone%prev => u%prev
  end function unit_clone


  !> integer powers
  function unit_int_pow(u1, p) result(u)
    implicit none

    type(unit_t), intent(in) :: u1
    integer, intent(in) :: p

    type(unit_t), pointer :: u

    if ( u1%cloned ) then
      ! TODO: should be able to handle complex cases like: a * (b * c)**3
      !       most likly, only updating the power of the linked list chain
      !       would do the job
    else
      u => unit_clone(u1)
    end if
    u%pow = u%pow * p
  end function unit_int_pow


  !> multiplication
  function units_mul (u1, u2) result (u2c)
    implicit none

    type(unit_t), intent(in), target :: u1, u2
    type(unit_t), pointer :: u2c


    if ( u2%cloned ) then
      if ( associated(u2%prev) ) then
        u2c => u2%prev%next
      else
        u2c => u2
      end if
    else
      u2c => unit_clone(u2)
    end if


    if ( u1%cloned ) then
      if ( associated(u2%prev) ) then
        u2c%prev => u1%prev%next
      else
        u2c%prev => u1
      end if
    else
      u2c%prev => unit_clone(u1)
    end if

    u2c%prev%next => u2c
  end function units_mul
end module units

一个pointer在Fortran中有三种可能的关联状态:

  • associated:指针实际指向一个definedallocated变量/匹配数据存储(其target);
  • disassociated:它是(或者是对象的一部分)显式 nullifieddeallocated,或者它的目标被正确地解除关联
  • undefined:与前者不同的任何东西,例如它的目标是(或成为)undefined,或者是 deallocated 通过其他方式而不是直接在指针本身调用 deallocate,其中其他原因。

当一个子程序的实例执行完成时(例如,当function units_mul到达end function时),任何未保存的局部变量变为未定义。此外,任何未保存或作为函数结果的 allocatable 局部变量都会被释放,并且当可分配实体被释放时,它也会变为未定义。

回到你的问题,u2cunits_mul 函数内的一个可分配的未保存局部变量,你将 u1c%next 关联到它。当此函数结束时,u2c 结束其生命周期并变为未定义,使 u1c%next 也变为未定义,在 Fortran 术语中称为 悬空指针 .

这是描述这种现象的 Fortran 标准的文本(尽管它指的是模块主机关联的情况,但逻辑相同):

Note 19.10

A pointer from a module program unit might be accessible in a subprogram via use association. Such pointers have a lifetime that is greater than targets that are declared in the subprogram, unless such targets are saved. Therefore, if such a pointer is associated with a local target, there is the possibility that when a procedure defined by the subprogram completes execution, the target will cease to exist, leaving the pointer “dangling”. This document considers such pointers to have an undefined association status. They are neither associated nor disassociated. They cannot be used again in the program until their status has been reestablished. A processor is not required to detect when a pointer target ceases to exist.

悬挂指针不是可靠的指针,编译器无法控制它。它们可能,出于任何原因,一直指向它们的最后一个内存地址(并且在某些情况下意外地给出了预期的结果,或者这些值将是随机内存地址的乱码),但它会最肯定的是 break,失败可以是任何事情,从错误的结果到 SIGSEG 错误 内存地址违规.

查看示例代码:

program dangling_pointer
  implicit none
  integer, pointer :: p(:)
  integer, allocatable :: a(:)

  call sub1(p)
  print *, 'sub1: ', p
  call sub2(p)
  print *, 'sub2: ', p
  call sub3(p, a)
  print *, 'sub3: ', p
  p => fun4()
  print *, 'fun4: ', p

contains
  subroutine sub1(dummy_p)
    ! the pointer passed as argument outlives the local target
    ! when the procedure ends, it becomes a "dangling pointer"
    integer, pointer :: dummy_p(:)
    integer, allocatable, target :: local_a(:)
    allocate(local_a(5))
    local_a = 100
    dummy_p => local_a
  end
  subroutine sub2(dummy_p)
    ! here the local variable is saved, so it persists. No problem here.
    integer, pointer :: dummy_p(:)
    integer, allocatable, target, save :: saved_a(:)
    allocate(saved_a(5))
    saved_a = 100
    dummy_p => saved_a
  end
  subroutine sub3(dummy_p, out_a)
    ! here the target is a passed argument, so it persists. No problem here.
    integer, pointer :: dummy_p(:)
    integer, allocatable, target :: out_a(:)
    allocate(out_a(5))
    out_a = 100
    dummy_p => out_a
  end
  function fun4() result(result_p)
    ! here the function result will be returned as a pointer. No problem here.
    integer, pointer :: result_p(:)
    allocate(result_p(5))
    result_p = 100
  end
end

使用 gfortran 9.0.0 我得到:

 sub1:     14316208           0    14287184           0         100
 sub2:          100         100         100         100         100
 sub3:          100         100         100         100         100
 fun4:          100         100         100         100         100

编辑

我认为这段代码可以解决您的问题:

allocate(u1c%next)
if (u2%cloned) then
  u1c%next = u2
else
  u1c%next = unit_clone(u2)
end if
u1c%next%prev => u1c