Puzzling behavior of "wrong" inner constructor

Today I was quite surprised by this mistake of mine in an inner constructor not throwing an error, but leading to a silent bug. My actual case had many arguments and I missed several, but here is a MWE:

julia> struct MyStruct{T}
           x::Float64
           MyStruct(x) = new{typeof(x)}() # forgot to put an argument here
       end

julia> s = MyStruct(1.0)
MyStruct{Float64}(1.0) # why did this work?

julia> s = MyStruct(1)
MyStruct{Int64}(6.9311367529162e-310) # where did this come from?

I guess the obvious takeaway is that one should always unit test the behavior of a custom inner constructor.

But I would have expected that calling a zero-argument new for this struct would throw an error, since the struct has a field.
Could someone explain the reasoning behind this?

1 Like

You can call new() with fewer arguments than the struct has fields and those fields will be uninitialized. For reference type fields that are represented as pointers, these appear as undef which is a non-value that’s an error to access in any way. For value type fields like you have here, the uninitialized fields will contain uninitialized memory, so basically just some random junk. (This junk value looks like it could maybe be a pointer reinterpreted as a float.)

2 Likes

Nope, it’s always allowed to call new() – it leaves all of the fields uninitialized. Admittedly that’s not often something you need to do in Julia (unlike C++ where it comes up all the time), but it’s a valid thing to do.

The fact that MyStruct(1.0) appears to work is actually really neat. The value of x is uninitialized (it’s just random garbage), but it seems to happen that Julia is re-using memory which contained that 1.0 for its uninitialized garbage. You get different uninitialized garbage when you pass 1, but it’s all just undefined behavior.

For example, passing a float64 gives yet other uninitialized garbage when in a function:

julia> function f()
         x = 1.0
         MyStruct(x)
       end
f (generic function with 1 method)

julia> f()
MyStruct{Float64}(6.9357062358239e-310)

So the fact that you got MyStruct{Float64}(1.0) back at the REPL is just a coincidence.

1 Like

Referencing Warn on uninitialized isbits-fields in structs? · Issue #24943 · JuliaLang/julia · GitHub.

Thanks for the explanations. I now see that this is mentioned in the manual. I think it is something that can happen accidentally, so while I’m probably not going to run into it myself again, I would also welcome a warning for new users as proposed in the linked issue.