Non conformant code but guaranteed to work?

On a recent SO Q/A it came that the following code was not standard-conforming:

program foo
USE iso_c_binding

integer, allocatable :: a(:)
character, allocatable, target :: c(:)
integer, pointer :: p(:)

integer :: n=1000, i

a = [(i,i=1,n)]
c = transfer(a,c)

! ... then later on ...

call c_f_pointer( c_loc(c), p, [n] )
write(*,*) all(p == a)   ! expected to print .true.

end program

It is non standard because for c_f_pointer the standard requires here c and p to have the same type (hence being just an equivalent of p => c), which is not the case.

C_F_POINTER(CPTR,FPRT,SHAPE): If the value of CPTR is the result of a reference to C_LOC with a noninteroperable effective argument X, FPTR shall be a nonpolymorphic pointer with10 the same type and type parameters as X
https://j3-fortran.org/doc/year/18/18-007r1.pdf

Yet it compiles without any warning/error with ifort and gfortran, and prints the expected result (.true.)… which does not mean anything about the conformance.

But I wonder : how could it go wrong in practice? I cannot imagine a case where it wouldn’t work, actually.

There’s a variation of this code, using an interoperable intermediate type instead of character:

integer(c_signed_char), allocatable, target :: c(:)

Which is still non conformant in theory, but with a different case:

C_F_POINTER(CPTR,FPRT,SHAPE): If the value of CPTR is the C address of an interoperable data entity, FPTR shall be a data pointer with type and type parameter values interoperable with the type of the entity.

For starters, TRANSFER semantics is such (weak/inadequate) a processor can possibly do things a programmer may not expect. A LOGICAL type is an immediate example where the attempted “round trip” can end up differently.

I have some production code like this which I also believe is portable in a practical sense but is nonconforming for the same reason. The things that I watch for are big- and little-endian differences and cases where there are bits in the output of transfer() that are not touched. I don’t think those apply to your code, so in a practical sense, it looks safe to me.

As you probably know, the only thing that is guaranteed by the standard is that if you have a statement like

c = transfer(a,c)

and then sometime later (or even in the same expression) have a statement like

b = transfer(c,b)

then a(:) and b(:), which are the same type and kind, should match. The standard does not say much else about what the intermediate c(:) looks like, and that is how your code works, it depends on how the bits are moved into c(:).

You might leave some test code in your application that is executed rarely, say once per execution, on some small cases, where you can check for consistency. Your assumption is that if the small test passes, then all of the other larger cases are correct. That doesn’t catch every possible failure, of course, but it could catch things like the above (big- and little-endian dependencies, sign bits, padding bits, and so on).

2 Likes

Indeed about c = transfer(a,c) the standard says:

If the physical representation of the result [c] has the same length as that of SOURCE [a], the physical representation of the result [c] is that of SOURCE [a]"

So let’s look at that:

call c_f_pointer( c_loc(c), p, [n] )     ! non-standard conformant
call c_f_pointer( c_loc(a), p0, [n] )   ! standard conformant

If there are discrepancies between p(:) and p0(:) it means that the physical representations of p0 and p differ a way or another. Hence that the physical representations of a and c differ: this contradicts the above citation of the standard. So, there shall not be any discrepancy between p and p0.

Is there something wrong in this reasonning?

I think in your example this length condition is not satisfied. At the time of the transfer(), c(:) is unallocated. Of course, the assignment itself then allocates c(:) to the correct length, but that happens after transfer() has done its thing. This might be just a technicality, but it might also be a way for padding bits or alignment to be introduced at the time of the transfer().

Just to give an example of a possible failure, supposed that integers must be aligned on 4-byte boundaries, but characters can be aligned at any byte. The c(:) array could then have an address that is not compatible with an integer address. In this case, the nonstandard

call c_f_pointer( c_loc(c), p, [n] )

might fail, perhaps silently, or perhaps with a seg fault.

1 Like

I don’t think this is issue. In the text of the standard the “result” has to be understood as the raw output of the transfer() command, regardless what it is assigned to. However…

Good point… And I think it answers my initial question: this is a case where it might fail.

I think you are right. I was thinking the length description was of the array c(:) as the mold argument, not of the result of the transfer() function, which itself involves some kind of memory allocation.

BTW, in the failure case, it might fail immediately, within c_f_pointer(), or it might fail some time later when the pointer p(:) is dereferenced and WW III is initiated.