What happens in struct construction

I wonder what is under the hood in the construction process of a struct.

For example:

struct A

    function A(::Type{Int64})
        return  new(Dict(1 => "one"), nothing)

    function A(::Type{Float64})
        a = Dict(2 => "two")
        return new(a,nothing)

    function A(::Type{String})
        return new(nothing, () -> show(field))

Basically, in the first case, is it true that I am directly constructing the dict to field in the first constructor and in the second one I am passing a pointer to field? Like push_back and emplace_back in C++'s std::vector?

Also, if I define a lambda as a member like in the third constructor. Does this lambda capture anything inside the struct? I think by default it does not. If I do a mutable struct instead I can then pass a pointer to lambda like (this = pointer(object)) -> blablabla where object is created before hand. This is not feasible in struct since we can’t change the fields of a struct. I wonder if there is any work around?

1 Like

For a start, a few points before the explaination,

  1. The two are the same
  2. Assignment to a (untyped) local variable is NEVER significant in julia. (The previous point comes out of this)

In this particular case, due to the ABI of the type Dict and because Dict is an abstract type, yes, a pointer is passed, and it is a pointer in both cases.

For a deeper explaination, you should note that julia and c++ has completely different object model. Similar to most scripting language, julia does not associate/equate an object with the variable (name) that refers it.

When you have auto a = T{}; in c++, a is the object and whenever you assign to a you are mutating that object. However, in julia, for a = T(), a is not the object, it merely refer to the object. Semantically (although not correct as a performance model) you can think of all julia variables/fields/array slots as a pointer to the actual object. You are always passing references (think pointers) of objects. This should explain why the two cases you have above makes no difference. Julia simply doesn’t have the copying problem C++ have. This doesn’t mean that julia never need to copy data around (more on that below) but that’s purely a compiler problem and after you specified how the data should be stored, the compiler has all the freedom to decide whether the copy needs to be done/whether the construction can be done in place.

(It is wrong semantics wise but implimentation wise you can sort of thinking all julia object always having a trivial copy constructor so the compiler can always elide that)

Now you will think this is inefficient and you’ll be right if the pointer is actually used all the time. However, a few things that helps.

  1. For local variable, the compiler can do whatever it want. This means that for a local variable of type Int, the compiler won’t actually generate that pointer and will do all the same tricks a c++ compiler can do.
  2. For fields and array slot, julia have immutable types. This gives the ABI freedom to not use the pointer (the reason for this is explained in the doc and I don’t really want to repeat although I could if you ask…) so the ABI will actually store a “copy” of the immutable object in a concrete type field/array slot.

(2) gives you control on the layout of your object (with limitations and a mutable object will never be stored inline) and if you put a big immutable struct inline to another struct, the compiler might be asked to copy data into it. However, as mentioned above, since the copy is always trivial, the compiler can usually elide that when it’s unnecessary (or when a c++ compiler can in a similar case).

Tthe capture rule is not affect by where the function is defined. It capture anything it needs from the local scope. Note that struct members is not a scope so accessing field as a variable won’t work. If you assign the result of new to a variable then accessing that variable from the function will give you access to the enclosing object.

You also never need to pass the pointer to the object, you can just capture the object directly. You can also do that for an immutable type since the captrue variable doesn’t have to be defined when you define the function, i.e. a = new(()->a) should work. If you want to emulate method call though, overloading getproperty will be better.


If I understand correctly, do you mean the following?

function A(::Type{String})
    fcn = nothing
    ret = new(nothing,fcn)
    fcn = (ret) -> show(ret.field)
    return ret

I think there is a big chance that I misunderstood you since this way we are actually changing fcn but the struct A is not mutable…

Update: I just realized you wrote something in the very end of your answer. I am gonna go ahead and try it. It is kind of incredible that

a = new(()->a)

works. I originally thought of it but dismissed it as I think rvalue always gets evaluated first… But I guess that I am too naive…

No. That will create a function where the member function member is nothing. Remember that assigning to a local variable never have any effect and since fcn is not used after the second assignment you code is equivalent to

    fcn = nothing
    ret = new(nothing,fcn)
    (ret) -> show(ret.field)
    return ret

Edit: ooops… Held Ctrl done when changing line…

What I meant is exactly the code I wrote. And I just saw you update your reply so I think it’s cleared now.

It’s pretty similar to & capture in c++. Except that you don’t need to declare the variable first (since you never need to declar the variable in julia anyway…)

thanks! I think I got some major refactoring to do :joy: