Fortran 2003 / 2008:优雅的默认参数?

Posted

技术标签:

【中文标题】Fortran 2003 / 2008:优雅的默认参数?【英文标题】:Fortran 2003 / 2008: Elegant default arguments? 【发布时间】:2016-10-09 23:44:30 【问题描述】:

在 fortran 中,我们可以定义默认参数。但是,如果不存在可选参数,则也不能设置它。当使用参数作为具有默认值的关键字参数时,这会导致像

这样的尴尬结构
PROGRAM PDEFAULT 

  CALL SUB
  CALL SUB(3)

CONTAINS 
  SUBROUTINE SUB(VAL)
    INTEGER, OPTIONAL :: VAL
    INTEGER :: AVAL ! short for "actual val"

    IF(PRESENT(VAL)) THEN
       AVAL = VAL
    ELSE 
       AVAL = -1   ! default value 
    END IF

    WRITE(*,'("AVAL is ", I0)') AVAL
  END SUBROUTINE SUB

END PROGRAM PDEFAULT

就我个人而言,我经常遇到不小心输入VAL而不是AVAL的问题,即接口中的变量名和代码中使用的初始化值之间的脱节会引入运行时错误——更不用说那个了这种初始化方式相当冗长。

有没有更优雅的方式使用带有默认值的可选参数?

示例 写类似

的东西会感觉更自然
IF(NOT(PRESENT(VAL))) VAL = -1 

因为它避免了 VALAVAL 的混淆。但它是无效的,大概是因为 Fortran 通过引用传递参数,因此如果 VAL 不存在于 CALL 语句中,则没有内存与 VALVAL = -1 相关联会导致段错误。

【问题讨论】:

好奇的细节:当使用一个可选参数而没有受到present() 的保护时,GFortran 和 Intel Fortran 都没有提供任何类型的编译时间警告。两者都只会在运行时因段错误而失败。 【参考方案1】:

你描述的情况相当好。我没有其他方法知道,这是符合标准的。具有类似名称的局部变量的模式是人们经常使用的。另一种选择是将if (present()) else 放在任何地方,但这很尴尬。

关键是它们是 可选 参数,而不是 默认 参数。 Fortran 没有默认参数。可能会更好,但这不是委员会成员在 80 年代准备 Fortran 90 时所选择的。

【讨论】:

【参考方案2】:

在研究这一点的同时,我发现您实际上可以使用OPTIONALVALUE 属性执行类似于建议示例的操作(至少对于 gfortran,不确定不同的编译器会如何处理它)。例如:

PROGRAM PDEFAULT 

  CALL SUB
  CALL SUB(3)

CONTAINS 
  SUBROUTINE SUB(VAL)
    INTEGER, OPTIONAL,VALUE :: VAL

    IF(.NOT. PRESENT(VAL)) VAL = -1 ! default value

    WRITE(*,'("VAL is ", I0)') VAL
  END SUBROUTINE SUB

END PROGRAM PDEFAULT

这是在 gfortran 4.9 版中实现的。这里是documentation for argument passing conventions中的相关解释:

对于 OPTIONAL 虚拟参数,不存在的参数由 NULL 表示 指针,INTEGER、LOGICAL 类型的标量虚拟参数除外, 具有 VALUE 属性的 REAL 和 COMPLEX。对于那些人来说,一个隐藏的 布尔参数 (logical(kind=C_bool),value) 用于表示 参数是否存在。

我还发现 this discussion 作为历史背景很有趣。

也许知识渊博的人可能会知道这样做是否是一个坏主意(除了依赖于编译器),但至少从表面上看,这似乎是一个不错的解决方法。

请注意,此行为不是 Fortran 标准的一部分,它取决于给定编译器的实现。例如,示例代码在使用 ifort(版本 16.0.2)时会出现段错误。

【讨论】:

相信这不是有效的 Fortran(这并不是说它不会在某些编译器/运行时实现人们在实践中想要的)。如果val 不存在,则不允许引用/定义它。这是if 声明。标准对使用不存在的虚拟参数的限制没有提到value 属性。 “可定义的匿名副本”仅适用于具有value 属性的当前虚拟参数的情况。 [不过,希望有人能证明我错了。] 嗯,我认为你是对的。早上我担心了很短的时间,但 bugzilla 线程说服了我。但是,他们在该页面上没有任何地方建议如果不存在引用该参数是允许的。 我同意这种行为既不是必需的,也不是标准的一部分。我从 bugzilla 讨论中的理解是,该标准允许 OPTIONALVALUE 属性,但没有关于如何实现它的指南。由于在具有VALUE 属性的 gfortran 参数中是按值传递的,因此我认为当缺少可选参数时,该变量仍然被分配但只是没有初始化(因此他们需要隐藏参数)。 另外,我确实用 ifort 测试了代码,它出现了段错误。我将在答案中澄清它不是标准的一部分,并且取决于实现。 这不是标准的部分是当它不存在时分配给 VAL。在我自己的代码中,我使用了原始帖子中的技术,我将评论说“省略的可选参数的默认值”是标准的下一个修订版(目前称为 F202X)正在考虑的功能。【参考方案3】:

虽然我当然不提倡在大多数情况下这样做(实际上在某些情况下你不能这样做),但有时可能会使用接口为具有不同要求的多个例程提供单个入口点 em> 参数,而不是使用可选参数。例如你的代码可以写成

MODULE subs
  implicit none
  public :: sub

  interface sub
    module procedure sub_default
    module procedure sub_arg
  end interface
 contains
  SUBROUTINE SUB_arg(VAL)
    INTEGER :: VAL
    WRITE(*,'("VAL is ", I0)') VAL
  END SUBROUTINE SUB_arg

  SUBROUTINE SUB_default
     integer, parameter :: default = 3
     CALL SUB_arg(default)
  END SUBROUTINE SUB_default
END MODULE SUBS

PROGRAM test
   use subs, only: sub
   call sub
   call sub(5)
END PROGRAM TEST

同样,我不推荐这种方法,但我认为无论如何我都应该将它作为一种替代方法来提供看起来像默认值的东西。

【讨论】:

【参考方案4】:

另一种可能性是使用关联块,它将局部变量名称与与可选参数同名的变量关联起来,例如。

SUBROUTINE SUB(VAL)
INTEGER, OPTIONAL :: VAL
INTEGER :: AVAL ! short for "actual val"

IF (PRESENT(VAL)) THEN
    AVAL = VAL
ELSE 
    AVAL = -1   ! default value 
END IF

ASSOCIATE (VAL => AVAL)
    WRITE(*,'("VAL is ", I0)') VAL
END ASSOCIATE

END SUBROUTINE SUB

不理想,但允许您在参数和例程主体中使用相同的变量名。想到我为应对可选参数缺少默认值而编写的大量不整洁的代码,我不寒而栗——在 F202X 上滚动。

【讨论】:

这当然有一些吸引人的功能,但也许你可以在更一般的情况下扩展这种方法的局限性? 限制是使用 associate 语句的限制 - 对可分配变量和指针变量的限制。此外,如果参数是 INTENT(OUT)(或没有明确的意图),则需要在关联块之后有相应的 IF (PRESENT(VAL)) VAL = AVAL。不过,这有点乱。可选参数的默认值会好得多。 您愿意edit 回答这些要点吗?【参考方案5】:

Fortran 标准库 (https://github.com/fortran-lang/stdlib) 提供了一个名为 optval 的函数,该函数在 stdlib_logger 中使用,例如:

subroutine add_log_file( self, filename, unit, action, position, status, stat )
    ...
    character(*), intent(in), optional :: action
    ...
    character(16)  :: aaction
    ...
    aaction = optval(action, 'write')
    ...
end subroutine add_log_file

所以他们表示“实际”值的方式是前置a

恕我直言,我喜欢带有附加 _ 的选项,因为可选值在调用签名中被直观地标记为这样。

【讨论】:

不错。值得一提的是,自己可以编写这样一个函数,它只是包含已经讨论过的 if 条件,没有新的魔法。 我查看了这个库的源代码。我的收获是,这主要是很好地证明了为什么 Fortran 需要类型泛型(“模板”)编程......【参考方案6】:

我希望 Fortran 支持一种流行的语法,比如

subroutine mysub( x, val = -1 )
integer, optional :: val

或者更多的 Fortran 风格

subroutine mysub( x, val )
integer, optional :: val = -1     !! not SAVE attribute intended

但这似乎不受支持(截至 2016 年)。所以一些变通方法需要用户侧完成......

在我的例子中,经过反复试验后,我决定在可选的虚拟参数上附加一个下划线,因此执行 (*) 之类的操作

subroutine mysub( x, val_)
integer, optional :: val_
integer val

其他人似乎喜欢相反的模式(例如,虚拟变量 => sep,局部变量 => sep_,参见 StringiFor 中的 split(),例如)。如this line 中所见,设置默认值的最短方法是

val = -1 ; if (present(val_)) val = val_

但是因为连这一行都有些冗长,所以我通常会定义一个类似的宏

#define optval(x,opt,val) x = val; if (present(opt)) x = opt

在一个通用的头文件中并用作

subroutine mysub( x, val_, eps_ )
    integer :: x
    integer, optional :: val_
    real, optional :: eps_
    integer  val
    real     eps
    optval( val, val_, -1 )
    optval( eps, eps_, 1.0e-5 )    

    print *, "x=", x, "val=", val, "eps=", eps
endsubroutine

...
call mysub( 100 )
call mysub( 100, val_= 3 )
call mysub( 100, val_= 3, eps_= 1.0e-8 )

但是,我认为这仍然远非优雅,只不过是在努力减少出错的可能性(通过在子例程的主体中使用所需的变量名)。


对于非常“大”的子例程的另一种解决方法可能是传递包含所有剩余关键字参数的派生类型。例如,

#define getkey(T) type(T), optional :: key_; type(T) key; if (present(key_)) key = key_

module mymod
    implicit none

    type mysub_k
        integer  :: val = -1
        real     :: eps = 1.0e-3
    endtype
contains

subroutine mysub( x, seed_, key_ )
    integer :: x
    integer, optional :: seed_
    integer :: seed
    getkey(mysub_k)   !! for all the remaining keyword arguments
    optval( seed, seed_, 100 )    

    print *, x, seed, key% val, key% eps
endsubroutine

endmodule

program main
    use mymod, key => mysub_k

    call mysub( 10 )
    call mysub( 20, key_= key( val = 3 ) )
    call mysub( 30, seed_=200, key_= key( eps = 1.0e-8 ) )  ! ugly...
endprogram

这可能有点接近某些动态语言在底层所做的事情,但在上述形式中这又远非优雅......


(*) 我知道使用 CPP 宏通常被认为是丑陋的,但 IMO 这取决于它们的使用方式;如果它们仅限于 Fortran 语法的有限扩展,我觉得使用是合理的(因为 Fortran 中没有元编程工具);另一方面,应该避免定义依赖于程序的常量或分支。另外,我想使用 Python 等来制作更灵活的预处理器(例如,PreForM.py 和 fypp 等)会更强大,例如,允许像 subroutine sub( val = -1 ) 这样的语法

【讨论】:

第一个解决方案是我在原始问题中发布的内容——更改命名约定并不能真正使其成为新模式。可悲的是,第二种解决方案是无效代码,一旦有多个可选参数,因为 fortran 不允许混合声明和执行代码 - 而对于单个可选参数,使用宏可能不足以证明晦涩难懂. 嗨,因为当前的 Fortran 标准不允许任何真正的“解决方案”(如下文 Vladimir 所建议的那样),除了使用具有相似名称的局部变量之外别无他法,我以为你正在寻找一些系统的(不易出错的)解决方法。 RE第二点,上面的代码是有效的(你可以试试),你也可以在key_前面添加其他常用的可选变量。关键是 setkey() 应该在所有其他声明之后,但这是很自然的,因为这种类型的关键字参数通常在所有可选参数之后。 另外,我的目的只是展示我的实践,而不是提倡它的用法。更重要的是,我的意图是强调当前的 Fortran 语法在某些部分是如何受到限制的,并强制用户编写相当冗长/冗长的代码。 [我添加了更多代码用于混合可选和“关键字”参数,但毕竟这些都相当丑陋(我不会使用第二个,虽然我经常使用第一个 optval() ).] 添加满足此目的的内容是“F202X”正在考虑的项目之一。目前还没有提出任何语法。

以上是关于Fortran 2003 / 2008:优雅的默认参数?的主要内容,如果未能解决你的问题,请参考以下文章

使用 LAPACK 的 Fortran2003 中的动态内存分配错误

在默认类型和 C 互操作类型之间转换 Fortran 字符和逻辑数组

在默认类型和 C 互操作类型之间转换 Fortran 字符和逻辑数组

Fortran 2008 - 条件编译[重复]

Fortran 2008 中的函数重载

系统地并行化 fortran 2008 `do concurrent`,可能使用 openmp