How to reference a variable within a struct

I understand that Julia doesn’t have “pointers” in the way that C++ does. However, I have stumbled upon an instance in which I feel like I really need something like a pointer that can refer to a variable from within a struct without actually copying that variable’s data into the struct itself. I want to know if there is a better or more Julian way to accomplish this task.

Here is a toy example that illustrates what I am trying to do. Suppose I have written a function that solves linear programs (LPs) in the form \max\lbrace{c^T x : Ax \leq b\rbrace}. The input data (c, A, b) for this function is given by the following struct:

struct LinearProgram
    c::Vector{Float64}
    A::Matrix{Float64}
    b::Vector{Float64}
end

Now, I am interested in what happens to the optimal x when a new constraint is added to this LP, so we have \max\lbrace{c^T x : Ax \leq b, \sum x_i \leq t \rbrace} instead. In particular, I want to vary the value of t and store the associated x-results somewhere. However, I will be doing this for various instances of (c, A, b), so I would like to make sure that each result is paired with the underlying LP data. Therefore, I created the following struct to store my LP results:

struct LinearProgramResult
    lp::LinearProgram
    t::Float64
    x::Vector{Float64}
end

and now my LP solver function looks like this:

function solveparametriclinearprogram(lp::LinearProgram, t::Float64)
    # Solve the linear program
    #   max    c⋅x
    #   s.t.   Ax ≤ b
    #          Σx ≤ t
    # and obtain the optimal solution x
    
    return LinearProgramResult(lp, t, x)
end

With this approach, I can store a big list of LinearProgramResults without worrying about losing the underlying data. However, because each LinearProgramResult contains the entire LinearProgram itself, if I want to try out several values of t for a single LP, then this seems to be a waste of memory.

Therefore, I would like to replace the second line of the LP result struct with a reference to an LP instead, with the understanding that the user modifies the referenced variable at her own risk.

struct LinearProgramResult
    lp::pointer(LinearProgram)
    t::Float64
    x::Vector{Float64}
end

Is this a reasonable thing to want to do? Is there a better approach I should be using instead?

Just make the Result mutable struct?.. btw the only thing that actually allocates are vector and matrix inside LinearProgram, but they are just references anyway:

julia> struct LinearProgram
           c::Vector{Float64}
       end

julia> struct LinearProgramResult
           lp::LinearProgram
       end

julia> l1 = LinearProgram([1,2,3]); L1 = LinearProgramResult(l1);

julia> L2 = LinearProgramResult(l1);

julia> push!(l1.c, 0)

julia> L2
LinearProgramResult(LinearProgram([1.0, 2.0, 3.0, 0.0]))

julia> L1
LinearProgramResult(LinearProgram([1.0, 2.0, 3.0, 0.0]))

so creating multiple Results from the same LinearProgram doesn’t copy recursively

3 Likes

Thank you, this is what I needed. Is this the right way of thinking about what making the struct mutable does? Namely, a mutable struct is a struct in which references variables at construction time are encoded as references to those variables rather than copies?

No. Mutable structures are structrues where the individual properties can be rebinded to other objects. The mutability property is irrelevant here:

a = [1,2,3]
mutable struct Foo
    x::Array{Int64,1}
end
o = Foo(a)
a[2] = 20
o # changed
struct Goo
    x::Array{Int64,1}
end
o = Goo(a)
a[2] = 200
o # still changed

If you DON’T want to use pointers, use copy or deepcopy. See also this tutorial for more details…

Maybe this will help you to form a mental model

Probably the key point here is that, when referencing the LinearProgram struct inside LinearProgramResults, you may be copying only scalars and references, but not the complete arrays contained in the LinearProgram:

julia> struct LinearProgram
           n::Int
           x::Vector{Float64}
       end

julia> lp = LinearProgram(3,zeros(3));

julia> struct LinearProgramResult
           lp::LinearProgram
           t::Float64
           x::Vector{Float64}
       end

julia> r = LinearProgramResult(lp,1.0,rand(3));

julia> lp.x[1] = -1
-1

julia> r
LinearProgramResult(LinearProgram(3, [-1.0, 0.0, 0.0]), 1.0, [0.05921563061434709, 0.7694750365923975, 0.0760104857787165])

Note that when one updated lp.x the r.lp.x array is updated, that is, they are the same array. Thus, r.lp.x is just a reference to the same array as lp.x.

Therefore, it won’t be that terrible if you just repeat lp inside every LinearProgramResult you construct, because the arrays won’t be copied. You have, however, to be careful exactly because of this, since mutating the value of one of this shared arrays anywhere will mutate them everywhere.

In other words, your initial tentative is not as bad as you would think, it won’t probably imply a huge memory footprint.