Hey! I'm a bit confused why some Ref
s have #undef
and others have a "zero" value as default:
julia> Ref{Any}()
Base.RefValue{Any}(#undef)
julia> Ref{Float64}()
Base.RefValue{Float64}(0.0)
julia> Ref{Bool}()
Base.RefValue{Bool}(false)
julia> Ref{Char}()
Base.RefValue{Char}('\0')
julia> Ref{Vector}()
Base.RefValue{Vector}(#undef)
Can I get a Ref{Bool}
with an undef value?
They are only #undef
if the Ref
is implemented with a pointer. #undef
represents a null pointer.
That's a little unsatisfactory because it's an explanation of the internals. The semantics are:
#undef
. Instead, uninitialized refs contain arbitrary data. This can be read safely - it is not undefined behaviour to access it. Reading the same uninitialized data twice will always result in the same value.Ref{T}
where T is not a bitstype) are always undef unless set. Accessing it will always throw an exception.So the answer is no: You can't have a RefValue{Bool}
with an undef value.
Do you need it for something?
Okay thanks!
My use case was:
computed = Ref{Any}()
function expensive()
isassigned(computed) && return computed[]
computed[] = let
# something expensive
end
end
and changing Ref{Any}
to Ref{Bool}
broke that code
Ah. In this case, I would use a Ref{Union{Nothing, Bool}}(nothing)
Jakob Nybo Nissen said:
So the answer is no: You can't have a
RefValue{Bool}
with an undef value.
Do you need it for something?
Are you sure? I'm not aware of there being any special initialization logic for bitstypes, the bitpattern is arbitrary, is it not? Not all bitpatterns are necessarily valid for all bitstypes, so reading this uninitialized memory is very much UB in the general case.
reading this uninitialized memory is very much UB in the general case
I think Julia uses the LLVM freeze
instruction to prevent UB from reading uninitialized memory. Some merged PRs found after a quick search:
Yes, it was made non-UB in Julia 1.11. Of course, it can still invoke undefined behaviour yourself if you create your own type which results in undefined behaviour if it doesn't have specifit bitpatterns. I.e. if you do:
struct NeverZero
x::UInt8
NeverZero(x::UInt8) = iszero(x) ? error() : new(x)
end
function foo(x::NeverZero)
if iszero(x.x)
call_undefined_behaviour()
else
x.x + 0x01
end
end
foo(Ref{NeverZero}()[])
I'm wondering now what the possible bitpatterns for Bool
in particular are when accessed through that RefValue
- I remember there being some cleanup to always make sure to have Bool
always be one of 0 or 1 as the bitpattern, but I don't know if that applies to this case as well.
Even in this example, it's not really correct to say that accessing the value is undefined behavior, is it? However, since the instance you get isn't guaranteed to satisfy invariants enforced by the constructor, using it may result in undefined behavior.
Well, it depends. Julia is a little bit more generous than C/C++ (due to optimizing a few things differently/less, ironically), but in principle there is no difference between "accessing" and "using" when it comes to UB. Anything at all may happen, including time travel! So just viewing it from the perspective of "but I haven't used that value for anything yet!" is not sufficient.
No that's not right. It is not UB to access the value. I.e. if you have a program that accesses the data but does not call an unsafe function, there is no UB. E.g. in my example, the UB is from the call to call_unsafe_behaviour
, not from loading the value.
W.r.t booleans, they can be stored as any bitpattern. Loading them will only read the lowest bit. On an LLVM level this happens with loading an i1, on an assembly level the byte is loaded, and then a mask is applied.
I wonder if it's possible to somehow invoke UB using the fact that booleans can be stored in memory as any bitpattern, although when loaded, they must always have 0x01 or 0x00. I can't think of a way, though
Maybe using unsafe_load
? Not sure though.
There can still be UB, even if you don't do anything with that value. That's exactly what the link I posted is about. I'm not sure it applies for the exact case of RefValue{Bool}
in Julia, but reading something that is not initialized is generally UB. It's still not merged, but https://github.com/JuliaLang/julia/pull/54099/files#diff-cad50cf26296590b1937056ff6064bd99292b8428c0fdff1db0f682e8a48551cR34 explicitly calls out the _existence_ of values other than 0x0/0x1 for a Bool
as UB. This is irrespective of what happens later on the LLVM level.
We should probably also check what the effects system says about reading from an uninitialized RefValue{Bool}
- I suspect the :noub
effect ist tainted.
Ah, and just a few lines below, it explicitly states that reading uninitialized bitstypes is not UB.. https://github.com/JuliaLang/julia/pull/54099/files#diff-cad50cf26296590b1937056ff6064bd99292b8428c0fdff1db0f682e8a48551cR46
What great consistency.
I think you're mistaken. Specifically:
1 + 1
is not UB, even though you can use the resulting value 2
to invoke UB.Bool
value as something other than 0x01
or 0x00
in Julia, even as the memory storing the bool is whatever. That is, I don't believe that it's UB to have Memory{Bool}
filled with e.g. 0xff
. The UB would be if you did something like reinterpret(Bool, unsafe_load(Ptr{UInt8}(pointer(mem)))
- i.e. if you actually created the Bool
. I don't think the Bool
really "exists" in the relevant sense before it's loadedSee:
julia> r = Ref{Bool}()
Base.RefValue{Bool}(false)
julia> Base.infer_effects(getindex, Tuple{typeof(r)})
(?c,+e,+n,+t,+s,?m,+u,+o,+r)
Summa summarum: As far as I can tell there is no way loading uninitialized data in Julia can cause UB, and I think we would need to see an example of this happening before it's worth considering as a problem.
That being said, it would be nice if we had something like: https://github.com/JuliaLang/julia/pull/54099#discussion_r1566982970
Last updated: Aug 14 2025 at 04:51 UTC