Search code examples
cmemory-managementrustdestructorownership

Rust Destructors and ownership


I almost asked the same question the other day but in context of c++.

I try to replicate destructors and constructors in my c programming. That means for every object or struct there is an initialization function and a destruct function which frees all of the objects resources like so:

struct MyObject {
  struct string a;
  struct string b;
  struct string c;
};

 void ConstructMyObject(struct MyObject *obj) {
   ConstructString(&obj->a);
   ConstructString(&obj->b);
   ConstructString(&obj->c);
}

 void DestructMyObject(struct MyObject *obj) {
   DestructString(&obj->a);
   DestructString(&obj->b);
   DestructString(&obj->c);
}

The destruct function is called at the end of every function scope just like in Rust only that i put it manually there instead of the compiler doing the job for me. So now in DestructMyObject function I call the destructors of every struct string type because for the struct string object i would also have a destruct function written just like for the struct MyObject Object. So everything that struct MyObject has allocated will be freed.

Example with my problem:

int main {
 struct MyObject Object1;
 ConstructMyObject(&Object1);
 ...
 ...
 ...
 TransferOwnershipFunction(Object1.b); /*takes a struct string object as argument*/
 ...
 ...
 ...

 DestructMyObject(&Object1);

 return 0;
}

I transfered ownersnip of a member (struct string b) of Object1 to another function. But struct string b will be freed by the main function because i have the rule that when an object goes out of scope i call its destruct function. But I don't want the main function to free this resource. TransferOwnershipFunction(...) is now responsible to free this member of object1. How does the Rust compiler deal with such situations? In Rust would i have to make a clone of string b?


Solution

  • The Rust compiler is smart enough to see when only a single field of a struct is consumed. Only that specific field has its ownership transferred and the remaining fields are dropped at the end of the scope (or otherwise consumed). This can be seen in the following example.

    struct MyObject {
        a: String,
        b: String,
        c: String,
    }
    
    fn consume_string(_string: String) {}
    
    fn main() {
        let object = MyObject {
            a: "".to_string(),
            b: "".to_string(),
            c: "".to_string(),
        };
        consume_string(object.b);
        // We can still access object.a and object.c
        println!("{}", object.a);
        println!("{}", object.c);
        // but not object.b
        // println!("{}", object.b);
    }
    

    (playground)

    However, if the struct has a non-trivial destructor, i.e., implements the Drop trait, then this can't happen. Trying to move a single field of the struct will result in a compiler error, as seen below.

    struct MyObject {
        a: String,
        b: String,
        c: String,
    }
    
    // This is new
    impl Drop for MyObject {
        fn drop(&mut self) {
            println!("dropping MyObject");
        }
    }
    
    fn consume_string(_string: String) {}
    
    fn main() {
        let object = MyObject {
            a: "".to_string(),
            b: "".to_string(),
            c: "".to_string(),
        };
        consume_string(object.b);
    }
    

    Attempting to compile this gives the error

    error[E0509]: cannot move out of type `MyObject`, which implements the `Drop` trait
      --> src/main.rs:22:20
       |
    22 |     consume_string(object.b);
       |                    ^^^^^^^^
       |                    |
       |                    cannot move out of here
       |                    move occurs because `object.b` has type `std::string::String`, which does not implement the `Copy` trait
    
    error: aborting due to previous error
    
    For more information about this error, try `rustc --explain E0509`.
    error: Could not compile `playground`.
    

    (playground) ([E0509])

    I think you already understand the reasoning for this, but it's worth repeating. If Drop is implemented for a struct, the destructor might have non-trivial interactions between the fields; they might not just be dropped independently. So that means that the struct has to stay as one coherent piece until it's dropped.

    As an example, the Drop implementation for Rc<T> checks if there are any (strong) references to the data left and if there aren't, drops the underlying data. If the fields of Rc<T> (a pointer, a strong reference count and a weak reference count) were dropped separately, there would be no way to check how many strong references were left when dropping the pointer. There'd be no way to keep the underlying data around if there are still strong references.

    As you guessed, in the case where Drop is implemented, you'd have to clone the field if you still wanted to consume it.