What is the most idiomatic way to implement bounds checking in `getindex` for arbitrary types?

Specifically, I have two main requirements in mind.

  1. It should throw BoundsError with complete information.
  2. The @inbounds macro should work as intended for this type, i.e. elide the bounds checking entirely

Item 1 seems straightforward, just conditionally throw a BoundsError with the right arguments. But, it seems that I am not sure about item 2; how does one check if @inbounds is bypassing bounds checking in the first place? I tried using @code_warntype and friends on @inbounds foo[i] where foo isa Foo, but it seems to give info about the operations of the inbounds macro itself.

A toy example.

struct Foo{T}
    x::Vector{T}
    y::Vector{Int}
end

import Base.getindex

function Base.getindex(f::Foo{T}, i::Int) where {T}
    idx = f.y[i]
    if idx  > length(f.x)
        throw(BoundsError(x, idx))
    end
    f.x[idx]
end

How can one transform this code to make @inbounds work as intended, and how does one check if it is working as intended?

Base.@propagate_inbounds function Base.getindex(f::Foo{T}, i::Int) where {T}
     idx = f.y[i]
     Base.@boundscheck idx in eachindex(f.x) || throw(BoundsError(x, idx))
     f.x[idx]
end

julia> function force_getindex(x, i)
           @inbounds x[i]
       end;

julia> x, y = [1, 2, 3], [1, 2, 3];

julia> resize!(x, 2);

julia> foo = Foo(x, y);

julia> foo[1]
1

julia> foo[2]
2

julia> foo[3]
ERROR: BoundsError: attempt to access 2-element Vector{Int64} at index [3]
Stacktrace:
 [1] getindex(f::Foo{Int64}, i::Int64)
   @ Main ./REPL[2]:3
 [2] top-level scope
   @ REPL[12]:1

julia> force_getindex(foo, 3)
3

Better to do

@boundscheck checkbounds(f.x, idx)

(Although if you do f.x[idx] it does the bounds check anyway, so Iā€™m not sure why the test is needed at all.)

1 Like

I guess particular example is too simple. The explicit bounds check logic that I am actually working on is bit more involved; even if the explicit check fails, the index would still point to a valid memory location i.e. would pass the bounds checking of the default types.

Should I update the question to include a more representative example?