Pointers to structs

Hi all,

I am having trouble defining simple structs and I would like your help!

The main issue is how to define a pointer to a struct of type Inner inside a struct of type Outer. So, the first idea is pretty obvious and looks like:

struct Inner
  i::Int64
  j::Int64
end

struct Outer
  u::Inner
  w::Int64
end

However, this approach leads to an initialization of Outer that needs an initialization of Inner:

obj = Outer(Inner(1, 2), 3)

In my case, I would like to initialize Outer objects without knowing the parameters that initialize the Inner object. To do this, the first idea it comes to my mind is making Outer mutable and changing its u parameter type to include Nothing:

mutable struct Outer
  u::Union{Inner,Nothing}
  w::Int64
end

So now, we can initialize using:

obj = Outer(nothing, 3)

And change the value of the Inner parameter later (once we know its parameters):

obj.u = Inner(1, 2)

But those two changes that I had to do (i.e. make the struct mutable and include the Nothing type) don’t look elegant to me. I would rather prefer to use a similar approach than C: use a pointer to a struct Inner inside Outer. This pointer can point to NULL meaning that it was not initialized or it can point to a struct of type Inner.

How can I achieve such behavior? Remember, I would like to keep the Outer struct immutable.

Maybe, one idea is to use a Vector. So let’s try it:

struct Outer
  u::Vector{Inner}
  v::Int64
end

and initialize using:

obj = Outer(Vector{Inner}(undef, 0), 3)

or, equivalently:

obj = Outer(Inner[], 3)

Later, I can push! an element:

push!(obj.u, Inner(1,2))

Is this the correct approach or there is a better one? Please, we aware that using a Vector{Inner} for u achieves the behavior of a list in C.

Thanks in advance!
Ramiro.

1 Like

why not:

struct Inner
  i::Int64
  j::Int64
end

struct Outer
  u::Ref
  w::Int64
end

julia> a = Outer(Ref{Inner}(), 3)
Outer(Base.RefValue{Inner}(Inner(4, 778529616)), 3)

julia> a.u[] = Inner(1,2)
Inner(1, 2)

julia> a
Outer(Base.RefValue{Inner}(Inner(1, 2)), 3)
1 Like

Yes, it seems to be a nice approach. I would only add the type of the reference:

struct Outer
  u::Ref{Inner}
  w::Int64
end

This way seems better because u cannot handle lists.

More ideas or insights are welcome!

Why not go all the way with genericity?

struct Outer{T}
  u::Ref{T}
  w::Int64
end

And of course the convenience method:

Outer(a::Ref{T}, b::Int64) where T = Outer{T}(a,b)

I think that using only Ref is like using a void pointer in C.

This pointer can point to NULL or to ANY other struct.

How can I achieve that this reference points to NULL or to structs of a given type (in this case, to Inner)?

I would like to initialize outer objects with u pointing to nothing and later make them point to Inner objects. This is because this way I can check if u is nothing, then it was not defined.

In my Vector example I can check that using the lenght of the vector.

In @Sukera example I cannot do it because I cannot change the type of the reference once an Outer object has been initialized.

Thanks!

Note that in Julia, structs of bitstypes are a bitstype, i.e. stored as a flat set of bits without a pointer. This is one of the reasons why it’s so efficient to use them vs standard objects! So that means that a struct of ints, or a struct of struct of ints, will not have a pointer.

This is really useful! Great!

For the porpoise of this example, let’s assume Inner is way more general and it does not include only Ints.

Well, you can always do

Outer(Ref{Union{<all the types you need>}}(), ...)

no?

Having an unlimited number of possible values there will lead to slower code because dynamic dispatch will be necessary. You could maybe use u::Any, but I doubt you want to dispatch everytime you need u :slight_smile: What core problem are you trying to solve? Maybe there’s a more julian way of doing this.

Note that this works:

struct Inner
  i::Int64
  j::Int64
end

mutable struct Outer
  u::Inner
  w::Int64
  function Outer(w::Int64) 
    o = new()
    o.w = w
    o
  end
end

Then

julia> obj = Outer(3) # Inner is random junk
Outer(Inner(0, 0), 3)

julia> obj.u = Inner(1,2);

julia> obj
Outer(Inner(1, 2), 3)

But you can’t check whether Inner has been defined this way. You’d need to add a flag, or do the union approach.
If Inner weren’t bitstype, it would show up as undef, and you could query whether or not it is defined with isdefined.

But if it’s all BitsTypes, you may as well make Outer a struct (not mutable), and construct new ones as needed. This will probably be the fastest of the options.

2 Likes

You could take a functional approach, leaving everything immutable, and use @set from Setfield.jl to rebuild the structs when you need to change something. You can even change the type of fields in the inner struct and Setfield will just rebuild the whole thing. And you get to keep the performance of immutable structs.

1 Like

Just a note, but I don’t know the relevance of this in actual code:

Ref{T} is still an abstract type. The two possible concrete types A where A<:Ref{T} are Base.RefValue{T} and Base.RefArray{T}.
So, in order for your Outer type to be concretely typed it should be defined as:

struct Outer
    u::Base.RefValue{Inner}
    w::Int64
end
2 Likes