#defer and errdefer

1 messages · Page 1 of 1 (latest)

midnight pollen
#

Hello, Noob Zig user here.
I just learned about defer and errdefer, and I have some questions:
So defer means "run this in the end" and errdefer means "run this if you return an error", so my questions are, can I have both defer and errdefer in a function and, if yes, do they execute in some sort of order or what happens?

eternal basin
#

errdefer and defer execute starting from the latest one, to the earliest one

#

so e.g., a defer at the beginning of the scope will run last, and the defer in the last bit of the scope will run first

#

it's kind of like how RAII works

#

just more general

#

and not tied to types

#

also, yes

#

you can both defer and errdefer

#

there's no restrictions on how you can use them

midnight pollen
#

So if I do

defer stdout.print("A", .{});
defer stdout.print("B", .{});
errdefer stdout.print("C", .{});
defer stdout.print("D", .{});

and i return a error, the output would be DCBA?

#

or CDBA?

eternal basin
#

without an error, that would be DBA, and with an error, that would be DCBA, yes

midnight pollen
#

okay, so they weight the same

eternal basin
#

yeah

#

you can think of errdefer as being defer if (there_is_an_error) expr;

midnight pollen
#

I kknow this is silly, but can I do a return in a defer or errdefer?

eternal basin
#

no

midnight pollen
#

figured, just checking >P

eternal basin
#

they run after the return

#

so e.g.,

var result: u32 = 0;
defer result += 1;
return result;

should return 0

#

because the deferred expression result += 1 runs after the return, not before

midnight pollen
#

hmmmmmmm

#

but

eternal basin
#

so it wouldn't make a lot of sense to be able to return afterwards

midnight pollen
#

there is no stack unwind yet

#

so why wont that change the return value?

eternal basin
#

what do you mean?

midnight pollen
#

lets say you are in a function

#

and you make a variable in that function, assign a value to it, and then return

#

when you create the variable, it gets pushed in the stack

#

and when you return from the function, stack unwinding happens, and the stack pointer is moved

#

so effectivly the variable is no longer there

#

if you can still reference the variable in the defer, why wont it change the return value?

#

since you are returning that variable

eternal basin
#

I imagine in terms of implementation details, it just does

const return_value = expr;
run_defers();
return return_value;
midnight pollen
#

must be something like that yeah, but that could have weird behaviour

eternal basin
#

not really

midnight pollen
#

what if the thing you are returning cant be copied/moved

eternal basin
#

I mean, if the thing you want to initialised can't be copied or moved, then initialising with a return statement is the wrong thing to do in the first place

#

even without any defers, there's not a way to directly refer to the result location of a function call through a return

#

so self-referential structs and the like have to be initialised via out-parameter

midnight pollen
#

still is weird since you are refering the variable in the defer, so you should be able to... mess with the variable, before you return

#

I would expect the return to be 1 in your example

eternal basin
#

I disagree

midnight pollen
#

also, defer is sold as a "cleanup code that runs in the end of the function"

eternal basin
#

it is

#

it is designed the way it is to accommodate the clean up patterns in zig

#

e.g.

var string = std.ArrayList(u8).init(allocator);
defer string.deinit();

// append, resize, and so on

return try string.toOwnedSlice(); // <- this moves the allocation out of the arraylist, making the `defer string.deinit()` a noop
#

if the defer string.deinit() ran before the return statement here, it would cause a UAF

#

or a double free, depending on the allocator behaviour

#

either way, it would be messy

midnight pollen
#

hold up I think i figured it out with it wont work

eternal basin
#

and severely worsen many APIs

midnight pollen
#

could it be because

#

the thing you are returning isnt the actual thing you are returning

#

since its a union

eternal basin
#

no, it's equally a copy without it being a union

midnight pollen
#

Im guessing that the return statement copies the thing to a union or something that can either be a error type or return type

#

it would make sense why the defer foo += 1; wouldnt change the value THAT WOULD BE RETURNED because it was changing the variable, but the value in te variable would already be copied to this thing that can either be a error or a value

#

kinda like what you told above

eternal basin
#

it doesn't have anything to do with it being a union

#

it's just copied because that's how return works

#

it doesn't return a stable memory addressed thing

midnight pollen
#

still i think that can be weird... like what if its a very huge object

#

it cant copy

#

it must move

#

but moving means it would be UB to change the variable value

eternal basin
#

if it's huge, you probably shouldn't have it on the stack to begin with

midnight pollen
#

well but thats a me concern, not a language concern

eternal basin
#

there just aren't complex move semantics in zig

#

there are already complex issues surrounding this topic

midnight pollen
#

i come from c++ and over there you can do pretty insane move stuff

eternal basin
#

most of that isn't to do with huge structs, and more to do with ownership semantics

midnight pollen
#

this is why I think that, if the return does behave like you explained, it could have some issues

#

expecially since they say we can use stuff from C++ in Zig

eternal basin
#

nobody has said that

midnight pollen
#

the creator did, in some talk

eternal basin
#

you've probably misunderstood

midnight pollen
#

that we can use C includes and C++ includes in zig

eternal basin
#

no, you can use C includes in zig with translate-c

#

and if there are C bindings for a C++ library, or your make C bindings yourself for a C++ library, then you can use the C++ library

eternal basin
midnight pollen
#

will do

#

btw, is zig oop?

eternal basin
#

no

midnight pollen
#

ahh, i see

eternal basin
#

zig is imperative, and maybe data oriented

midnight pollen
#

so it really is just a modern C?

eternal basin
#

that is all it is marketed as, yes

midnight pollen
#

ok ok

eternal basin
#

at any rate, to respond to the idea of it being problematic in the case of huge structs: if your struct is huge enough that copying it would cause stack overflow, having it on the stack in the first place is already going to cause you problems without copying it

#

so the solution to having a huge struct on the stack is to not have it on the stack in the first place

midnight pollen
#

Yeah, I was just trying to see how that would behave with the C++ functionality

eternal basin
#

there is no builtin C++ functionality

midnight pollen
#

but even then

#

I maybe dont want to copy

eternal basin
#

then don't

midnight pollen
#

I think that Zig should consider that

eternal basin
#

use an out pointer

midnight pollen
#

well I mean, I might not want to copy more then the thing I rwrite in code

eternal basin
#

and it turns out it isn't that simple

#

Like I said

#

if you really want a huge ass struct on the stack and don't want to heap allocate it

#

only refer to it by pointer, and initialise it through an output pointer

midnight pollen
#

It could be a small struct

#

I was talking about, the code doing what I write it to do

eternal basin
#

it does do what you write it to do

midnight pollen
#

well, yes, but more

eternal basin
#

it can't be "more"

#

those are just the semantics of the code, and it is what everyone currently using zig expects it to do

#

your way is less useful, and breaks common patterns already in use

midnight pollen
#

I'm talking about the thing you told, that is, you want to return a variable, it copies that variable (at the return point) to another place, and if you reference the variable in a defer or errdefer (i assume), it will reference your original variable, and any changes made to it wont affect what is being returned)

#

meaning, if there is something you want to do on the variable before you return it, regardless of the place in code, you must have code before each return stament

#

because you wont be able to use that in the defer or errdefer

eternal basin
#

there are no patterns in zig that really necessitate that

#

if you want the result in my example to always be at least 1, just make it equal to 1 on initialisation

#

no defer shenanigans needed that way

#

if you want to assert that the result meets certain condition, you can still do that, since that doesn't require modification at all

#

zig doesn't do things just because you want to be able to do so

#

it serves specific, useful use cases

midnight pollen
#

okay this is silly but let's say that you wanted something like this:

fn foo(....) {
  var n: i16;
  defer n = if(n % 2) 1234 else 4321;
  // mess with n
  return n;
}
eternal basin
#

that doesn't seem useful at all

#

and in this example, you can just but it before the single return statement

midnight pollen
#

if I had several return locations

#

I would need to do that in every on of them

eternal basin
#

if you happen to have multiple return statements, you can abstract it into a function

midnight pollen
#

and I though the point of defer was to avoid that

eternal basin
#

the point of defer is to simplify cleanup code

#

which its current semantics do perfectly

midnight pollen
#

So I should use it exclusivly to clean resusrces used by the function?

#

Not to run arbitrary code in the end?

eternal basin
#

it can be used for other purposes, e.g. errdefer |err| assert(diagnostic_ptr == null or diagnostic_ptr.?.err == err);

#

but it was never designed to run arbitrary code at the end for modifying your return value

midnight pollen
#

I see, is a compiler error generated if I do it?

eternal basin
#

refactoring that would take longer than just having used something like return transformIntoReturn(result); at every call site to begin with

eternal basin
#

zig doesn't have a language-level concept of resources

#

it's all encoded in the patterns we use

midnight pollen
#

I see

eternal basin
#

a compiler doesn't recognise if a defer is a cleanup procedure or an assertion or just a variable modification

#

and modifying variables can actually be a part of a cleanup procedure

#

e.g., hashmap.deinit() deallocates the hash map, and invalidates the value (sets it to undefined)

midnight pollen
#

some variables, sure, but the one we return it has no effect

eternal basin
#

the compiler doesn't know that

#

so isssuing a compile error for that would inevitably create false positives

#

anyway, this thread has gone on long enough. I highly recommend you read up on the topic

midnight pollen
#

Okay, I will read what you shared with me so far, thank you for replying :)