While I was unaware of the absence of runtime bounds checking with explicit shape arrays and agree it gives a false sense of guarantees, I believe the correct way to use explicit-shape arrays is using variables/parameters and not “magic constants”, i.e.
subroutine sub(n, X, Y)
integer, intent(in) :: n
! Document any requirements on shape n
real, intent(in) :: X(n,n)
real, intent(out) :: Y(n,n)
...
end subroutine
integer, parameter :: sz = 5
! Document why this size was needed
allocate(A(sz,sz), B(sz,sz))
...
call sub(sz, A, B)
Another example is a subroutine to process a point-cloud. The point coordinates could be stored either as a 3×N array (x1,y1,z1,…,xN,yN,zN) or an N×3 array (x1…xN,y1…yN,z1…zN).
If my subroutine has the interface
subroutine process_points(xyz)
real, intent(in) :: xyz(:,:)
then I need to read the documentation or more likely the routine itself (since undocumented) to figure which storage order is used.
On the other hand the interface
subroutine process_points(n,xyz)
integer, intent(in) :: n
real, intent(in) :: xyz(3,n)
communicates immediately that the routine expects a 3×N array and I can allocate the storage accordingly.