3
votes

I am experiencing an allocation failure when using allocatable character strings as optional arguments. The problem only occurs when I call through two levels of procedures. In my actual code call get_level1() (see below) represents a call to a list data structure and call get_level2() represents the list calling the same type of accessor function on one of its records. I have stripped down an example to the bare minimum that adequately reproduces the problem.

In the code below when I call get_level2 directly the expected character string is returned through the optional argument. When I call get_level1 which in turn calls get_level2 allocation of the optional dummy argument fails. Using gdb I find the allocation attempt to create a character*1635... when it gets back to the actual argument is obviously has an integer overflow because it thinks the allocation is character*-283635612...

My actual code has many optional arguments not just one. As a simple example I added an optional integer argument. This time instead of a segmentation fault I get a null string.

In the second example the integer argument works regardless of using the character argument. (I would expect this since no dynamic allocation is being performed) The integer's presence has no effect on the character. I have also tried changing the intent to (inout). This does not change the behavior, though I did not expect it to. [I believe that intent(out) causes the actual argument to deallocate first, and intent(inout) retains the actual argument's allocation state]

call get_level1( NUM=n )              ! works
call get_level1( NUM=n, TEXT=words )  ! fails
call get_level1( TEXT=words )         ! fails

my compile cmd is:

gfortran -Wall -g -std=f2008ts stest1.f08 -o stest

Environment

Linux 4.15.0-42-generic #45-Ubuntu SMP x86_64 GNU/Linux
GNU Fortran (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0

Example with one optional argument

module stest1
  implicit none

  character(:), allocatable :: data

contains

  subroutine get_level2( TEXT )
    implicit none
    character(:), optional, allocatable, intent(out) :: TEXT

    if ( PRESENT( TEXT ) ) then
       TEXT = 'Prefix: ' // data // ' :postfix'
    end if

  end subroutine get_level2


  subroutine get_level1( TEXT )
    implicit none
    character(:), optional, allocatable, intent(out) :: TEXT

    call get_level2( TEXT )

  end subroutine get_level1

end module stest1


program main
  use stest1
  implicit none

  character(:), allocatable :: words

  data  = 'Hello Doctor'

  call get_level1( words )

  write(*,100) words

100 format( 'words = [',A,']' )

end program main

Example with two optional arguments

module stest2
  implicit none

  character(:), allocatable :: data
  integer                   :: count

contains

  subroutine get_level2( TEXT, NUM )
    implicit none
    character(:), optional, allocatable, intent(out) :: TEXT
    integer,      optional,              intent(out) :: NUM

    if ( PRESENT( TEXT ) ) then
       TEXT = 'Prefix: ' // data // ' :postfix'
    end if

    if ( PRESENT( NUM ) ) then
       NUM = count
    end if

  end subroutine get_level2


  subroutine get_level1( TEXT, NUM )
    implicit none
    character(:), optional, allocatable, intent(out) :: TEXT
    integer,      optional,              intent(out) :: NUM

    call get_level2( NUM=NUM, TEXT=TEXT )

  end subroutine get_level1

end module stest2


program main
  use stest2
  implicit none

  character(:), allocatable :: words
  integer                   :: n

  count = 42
  data  = 'Hello Doctor'

  call get_level1( TEXT=words )

  write(*,100) words
  write(*,110) n

100 format( 'words = [',A,']' )
110 format( 'N     = [',I0,']' )

end program main
2
Welcome to StackOverflow. Don't forget to take the tour and read How to Ask, if you didn't already. What is your compiler version? Did you get the same with another compiler?Rodrigo Rodrigues

2 Answers

4
votes

You seem to have hit a compiler bug. I can reproduce the issue on gfortran 8.2.1:

Operating system error: Cannot allocate memory
Memory allocation failure in xrealloc

Error termination. Backtrace:
#0  0x7f9c0314f107 in write_character
    at ../../../libgfortran/io/write.c:1399
#1  0x7f9c03153e66 in list_formatted_write_scalar
    at ../../../libgfortran/io/write.c:1872
#2  0x400c78 in MAIN__
    at /tmp/test.F90:43
#3  0x400cbe in main
    at /tmp/test.F90:34

but in 5.1.1 I see the correct output:

Prefix: Hello Doctor :postfix

With the following work-around, I got it to work:

  subroutine get_level1( TEXT )
    implicit none
    character(:), optional, allocatable, intent(out) :: TEXT

    character(:), allocatable :: tmp

    if ( PRESENT( TEXT ) ) then
      call get_level2( tmp )
      TEXT = tmp
    else
      call get_level2( )
    endif

  end subroutine get_level1
3
votes

It is a bug in the compiler, and still stands in gfortran v9.0.0 (experimental) on Windows. You should report it with the vendor.

I've done some tests and it seems that the failure only happens when: passing a present optional argument as an actual argument corresponding to a dummy argument that is character(:), allocatable, optional. Any variation in the previous sentence seems to avoid the bug and produce the correct result.

I reduced your example to a minimal test-case:

program main
  implicit none
  character(:), allocatable :: txt

  call sub1(txt)
  print *, "main ", len(txt), txt      ! prints: main 0 (or throws segfault)

contains
  subroutine sub1(txt)
    character(:), allocatable, optional :: txt
    call sub2(txt)
    print *, "sub1 ", len(txt), txt    ! prints: sub1 0 (or throws segfault)
  end
  subroutine sub2(txt)
    character(:), allocatable, optional :: txt
    if(present(txt)) txt = "message"
    print *, "sub2 ", len(txt), txt    ! prints: sub2 7 message
  end
end

The inspection inside sub2 shows that the assignment actually works there. The problem seems to happen when associating that dummy to the actual argument inside sub1. Hmm...

Again, any variation of the pattern character(:), allocatable, optional dummies produces the correct result in my tests. So, I suggest you to flexibilize at least one of the previous conditions to circumvent the buggy thing. There are some suggestions:


1. non-allocatable optional character works, no matter if fixed or assumed length;

Here is an example with fixed-lenght variable and assumed-length arguments.

Advantage: Easy to refactor, less disruptive/intrusive.

Disadvantage: Must estimate the length of the variable beforehand, wastes storage.

program option1
  implicit none
  character(10) :: txt

  call sub1(txt)
  print *, "main ", len(txt), txt      ! prints: main 10 message

contains
  subroutine sub1(txt)
    character(*), optional :: txt
    call sub2(txt)
    print *, "sub1 ", len(txt), txt    ! prints: sub1 10 message
  end
  subroutine sub2(txt)
    character(*), optional :: txt
    if(present(txt)) txt = "message"
    print *, "sub2 ", len(txt), txt    ! prints: sub1 10 message
  end
end

2. non-optional, on either the actual argument passed from sub1 or on the dummy argument in sub2, also makes it work;

Of course, if you can refactor your code to avoid this situation, that would be the better solution. You could use generic interfaces to achieve a similar result, for example. Or, as you said in the comment, "using local variables at level1 and passing all the optional arguments to the lower level".

Disadvantage: May need to change interfaces of the lower-level procedures.

Advantage: Wouldn't be a problem if they are private module procedures; It's an implementation detail.

Consider the following approach, that hacks the bug and avoid passing an optional argument, so doesn't change the procedure's signature:

program option2
  implicit none
  character(:), allocatable :: txt

  call sub1(txt)
  print *, "main ", len(txt), txt      ! prints: main 7 message

contains
  subroutine sub1(txt)
    character(:), allocatable, optional :: txt
    character(:), allocatable :: txt_

    if(present(txt)) then
      ! txt_ isn't optional, so the bug doesn't fire
      call sub2(txt_)
      txt = txt_
    end if
    print *, "sub1 ", len(txt), txt    ! prints: sub1 7 message
  end
  subroutine sub2(txt)
    character(:), allocatable, optional :: txt
    print *, present(txt)
    if(present(txt)) txt = "message"
    print *, "sub2 ", len(txt), txt    ! prints: sub2 7 message
  end
end

3. with any other type it works too, no matter the attributes (even a derived-type with allocatable character component). Although, changes on the rank or kind don't count.

I will show you two options involving derived types: one with allocatable character length component; the other with parameterized derived type.

Advantage: You can keep your code structure, and all the optional stuff. Storage overhead is low. You could even extend your DT with methods and tailor it to your problem.

Disadvantage: Maybe too much hassle for little. PDT is cool, but is a new (and buggy) feature in gfortran.

program option3a
  ! using a derived type with allocatable character length component.
  implicit none

  type :: string
    character(:), allocatable :: chars
  end type

  type(string) :: txt

  call sub1(txt)
  print *, "main ", len(txt%chars), txt%chars   ! prints: main 7 message

contains
  subroutine sub1(txt)
    type(string), optional :: txt
    call sub2(txt)
    print *, "sub1 ", len(txt%chars), txt%chars ! prints: sub1 7 message
  end
  subroutine sub2(txt)
    type(string), optional :: txt
    if(present(txt)) txt = string("message")
    print *, "sub2 ", len(txt%chars), txt%chars ! prints: sub2 7 message
  end
end

program option3b
  ! using a parameterized derived type, you can practically mimic the intrinsic 
  ! character type behavior, with the possibility to add custom behavior.
  ! but its still raw in gfortran.
  implicit none

  type :: string(len)
    integer, len :: len
    character(len) :: chars
  end type

  type(string(:)), allocatable :: txt

  call sub1(txt)
  print *, "main ", txt%len, txt       ! prints: main 7 7 message (a lil bug of gfortran)

contains
  subroutine sub1(txt)
    type(string(:)), allocatable, optional :: txt
    call sub2(txt)
    print *, "sub1 ", txt%len, txt     ! prints: main 7 7 message
  end
  subroutine sub2(txt)
    type(string(:)), allocatable, optional :: txt
    ! the following fails with gfortran, however it's valid syntax
    ! if(present(txt)) txt = string(7)("message")
    allocate(string(7) :: txt)
    if(present(txt)) txt%chars = "message"
    print *, "sub2 ", txt%len, txt     ! prints: main 7 7 message
  end
end

Summing up: you can change your compiler or choose any of those (or other) way to circunvent this bug and keep working, until your compiler vendor address the issue.