Check out a free preview of the full The Rust Programming Language course
The "Ownership" Lesson is part of the full, The Rust Programming Language course featured in this preview video. Here's what you'd learn in this lesson:
Richard discusses how Rust assigns an owner when it makes an allocation, how return transfers owners, the use-after-move compiler error, and .clone(). A student questions regarding if it's enough to just return the variable is also covered in this segment.
Transcript from the "Ownership" Lesson
[00:00:00]
>> So ownership. Let's say I have this function called get_years, which returns a vec of i32 and I instantiate the vec inside here. So it doesn't take any arguments, it just makes a vec and returns it. So far so good, this seems like a pretty straightforward function. I then let's say main calls it.
[00:00:16]
Well, this is actually a little bit more complicated because of what we just learned. So we know that vec will cause an allocation to happen naturally. And then we also know that years is gonna go out of scope right here. Which means right here, it's going to deallocate years before the function returns.
[00:00:32]
Well, now that's a bit of a problem. [LAUGH] Because, yeah, I mean, I agree that years did go out of scope and, of course, that triggers a deallocation. But now we're gonna have a use-after-free and main as soon as we use this thing. Because the thing has been de allocated and yet, we still have a way to reference it.
[00:00:49]
So if Rust had only the heuristic of deallocate when everything goes out of scope, this would be a problem. It would lead to use-after-free bugs. But, of course, we don't wanna have use-after-free bugs. So fortunately, Rust has a way around this. And this is the first of several edge cases that we're gonna talk about in Rust's.
[00:01:06]
Otherwise, quite simple strategy for high performance automatic memory management. Okay, so basically, whenever Rust does an allocation, it assigns an owner to that particular allocation. And the owner is basically the scope where the allocation was created. So basically, when it does, its allocations says okay, this current scope that we're in, it owns years.
[00:01:30]
And then when main calls this function, it basically says okay, I'm going to transfer ownership from this scope to main scope, because we returned it. So returning transfers ownership from one scope to another. And ownership essentially refers to whose responsibility is it to deallocate this thing? Whenever the owner exits the scope, either it needs to transfer that ownership to somebody else.
[00:01:56]
Or if there's nobody else to transfer to, then it will deallocate that memory. So because we're returning this value here, it says, okay, got it, you wanna return this thing. I'm not gonna deallocate it because I'm gonna transfer ownership domain and now it's main's responsibility to deallocate it.
[00:02:10]
Colloquially in Rust, this is referred to as moving. A move in Rust refers to transferring ownership from one scope to another. So here we would say, we're gonna move years from get_years to main. And then here when main is calling this thing, it's actually taking ownership of that value.
[00:02:27]
And then from now on, this years thing, whenever it goes out of scope, we're gonna be in the same boat. Either it needs to have its ownership transferred to a different scope or else main is going to deallocate it. And since main doesn't transfer it to anything else cuz main doesn't return, it's going to deallocate it when main goes out of scope.
[00:02:43]
Great, this is exactly what we wanted. Because now we have successfully allocated the memory, it got D allocated automatically and we're good. Okay, cool, so again, we still have a guarantee you no-use-after free and no double free and no GC pause. Okay, so now let's see we have a function called print years which takes years which is a vec of i32s.
[00:03:09]
Iterates through them and prints all the years, very straightforward function. This is going to take ownership of years from the caller. So basically the caller is going to say, hey, I'm gonna give you years and you're now responsible for cleaning up after this thing. So when years goes out of scope at the end of this function, we're not going to deallocate it.
[00:03:29]
So if I have my main function here, I've got a years vec that I defined inside main. I call print years, passing years. So it's great that is a move, I am now saying years had his ownership transferred into this print year scope. And we get to the end of this function, we're gonna deallocate years, that's all done.
[00:03:44]
So this is no longer main's responsibility to clean this up even though main is where it was allocated. But now let's say we call print years again. Like I wanted to print the years and then that wasn't good enough. I wanna print them a second time. Well, now we have a bit of a problem.
[00:03:59]
Because after I called print years the first time, years got deallocated. And then I called it again, and now, use-after-free bug. When we come in here, I'm now using this memory that was deallocated the previous time this is printed. So this is a problem. How does Rust prevent this use-after-free bug from happening?
[00:04:18]
Well, basically, Rust has a compiler error for this. Like in other languages, this might happen in different things. But the way that Rust does it is it says, hey, the way that you're dealing with this memory management system, this is not allowed. And the exact error that you'll see is they'll say use have moved value.
[00:04:34]
Namely years, and it'll say the value is moved here. So on line 10, let's say this is line 10. And value used here after move on the next line. So we use after move is the first sort of borrow checking error that we'll see from Rust. Use-after-move is not a compiler error that you'll ever see in any other language.
[00:04:52]
But this is why we've sort of been building up this mental model of how these things work. This is why Rust is doing it. The reason that you will see a use-after-move value is that the compiler is trying to prevent you from getting a use-after-free bug. And the way that it's doing this is by telling you, hey, if I go and do my normal thing with automatically delegating, when this thing goes out of scope, you're going to get a use after free bug for sure.
[00:05:15]
And I don't want that to happen to you, so I don't know what you wanna do instead. But I'm just gonna tell you, this is an error and you got to figure out some other way to do that. Okay, fair enough, thank you, Rust compiler. I appreciate it.
[00:05:26]
So how can we actually deal with this and convince it not to free that memory when it normally otherwise would, based on the simple rules we've learned so far? Okay, well, we started the whole problem here was that at the end of print years, it's deallocating years. So why is it deallocating?
[00:05:40]
Well, because it went out of scope, right? And we didn't move it somewhere else. But we couldn't move it somewhere else. We could just throw out a return value here and say, hey, once I'm done with years, just return it. As we saw in an earlier example, if you're returning something, you actually transfer ownership of that thing back to the color.
[00:05:58]
So that would work, then it wouldn't deallocate anymore. Instead, it would transfer ownership back to the color. That would totally work so that our main function would have to look a little bit different. So we first define years, then we'd say something like let years to equals print years.
[00:06:11]
So this would actually say, okay, I'm gonna transfer ownership into print years. Then it's gonna do its thing, printing out all the years, then it's gonna return it back to me. Now I have this year as 2, which I can then pass to another print years, which is going to then give me back years 3, and then I can use that subsequently if I want to.
[00:06:27]
Now this might seem a little bit silly, but it does solve our problem. Now granted the next section, we're gonna see a much more elegant way to do this. But conceptually, based on what we've learned so far, this is a perfectly reasonable workaround. So this is a tool to have in your arsenal.
[00:06:40]
If you encounter a use-after-move compiler error, and you're looking at that saying, use-after-move, okay, how do I fix that? This is one way you can do that, is you can do something that actually intentionally triggers a move like returning the value from the function and we're gonna practice this in the exercise.
[00:06:58]
So you're actually intentionally transferring ownership back into the caller's scope so the caller can access it again later on. Let me pause here and ask questions about use after move.
>> Would it be possible to not bind the return value of print years and just past years again and just use the return in the previous function as a hack?
[00:07:18]
But you don't really need years two years three because because you aren't mutating years in the print.
>> That's a great question. Yeah, so basically like, okay, is it enough to just have the hack where we return years? Do we actually need to say let years two equals or could we just leave this part out?
[00:07:32]
Short answer is you do actually need to put it in here. Basically, the reason for that is that this original years is sort of like you can think of it as being consumed once you pass into this function. Once you hand over ownership, that thing is gone. And if you want to get back ownership of another one of those values, you got to like assign this equal to and then this one has not been moved yet.
[00:07:51]
But, basically, if you don't do that, you'll still get the same error as before. So you do actually need to bind this to give it a new name. Or actually, you can use the same name again if you want but that's intentional shadowing. We'll go down that rabbit hole but, yeah, so you do need to actually rebind it.
[00:08:09]
In this final section, I like to call clone is your friend. So here's what I mean by that. There's another way we can deal with this. So go back to our previous example of print years or we take years, iterate through it, and then at the end it gets reallocated because it goes out of scope.
[00:08:25]
Rather than doing this whole return value dance, another thing we could do is just this years.clone, taa-daa, problem solved. So the .clone method basically says make a complete copy of this thing like duplicate all of its contents like this is like a deep clone. Just go through everything and just clone it all.
[00:08:44]
And basically, what it's saying is, okay, yeah, print years is gonna deallocate whatever I give it, that's fine. I'm gonna give it a clone, which means that my original years is still in scope in main. I'm just handing off this clone and I'm creating it on the fly and handing it to print_years.
[00:08:58]
And then, yeah, when print_years gets to the end, it's gonna deallocate the clone but I don't care about that. I'm never gonna use it again. And then I still have access to years which I can then use again. Or if I wanted I could clone it again, then use years even further down in May.
[00:09:11]
So upside, very quick. If you ever see a use after move nine times out of 10, you can solve it by adding like a single.clone call. Downside is pretty obvious. There's a performance cost to using clone. [LAUGH] And, of course, the whole reason we're using Rust is we want to be able to go really fast.
[00:09:28]
The reason I say clone is your friend is when you're a beginner, it's very common to encounter errors that you've just never seen before in any other language and that can be frustrating. You can get stuck. This is a way to get you unstuck and I can say from experience and not just personal experience but also talking to other people.
[00:09:45]
If you use clone as a way to get around use-after-move errors, later on when you get better at Rust. You've spent some more time with it and you start to learn the tips and tricks and the techniques. And these errors are no longer things you've never seen before, but it's like, I've seen these before.
[00:09:58]
I actually have learned some ways to deal with them. You can just go back and remove the clones and use the techniques like the ones we're gonna talk about in the next section. And just get rid of those clones. So this is basically a way to say I'm gonna temporarily sacrifice some performance because I'm a beginner.
[00:10:12]
I don't really know all the great techniques to do this in a high performance way. But later on, I'll always be able to come back and clean them up and remove them and sort of restore the performance. And still have no concern about use after freeze or double freeze or anything like that.
[00:10:30]
So and the exercise are actually do the same thing both ways. We're gonna do it with and without clone, so you can see a way to do it using the workaround we just talked about. And I'm gonna see a way to do it using clone. And then also, in the next section, we're gonna learn another technique to do it that's more elegant than either of them.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops