Wrong terminology used in Rust (drop/destructor/finaliser)


#1

Hi there,

I’m coming from the Java world and some terminology used in Rust seems wrong to me. I’m talking about the drop(&mut self) function declared in the Drop trait. Rustaceans call this function a destructor and this seems wrong to me. Although the Rust programming language doesn’t have a garbage collector it relates to languages with automatic memory management. It just uses another approach to manage the memory automatically. Thus the work the drop(&mut self) function should do is smaller than the work a destructor in a similar C++ code should do. The main difference is that the drop(&mut self) function doesn’t need to destruct any member object. By object destruction I mean what the delete operator in C++ does.

I think the right description name of the drop(&mut self) function should be a finalizer and not a destructor. The functionality of the drop(&mut self) function seems to me solely as an object finalization before a destruction of any part of it. Just like the functionality of the finalize() method in Java. The only difference is that in Java the finalize() method call is non-deterministic because it’s called by the garbage collector.

Even Brian Anderson from Mozilla was confused between those description names of drop in Rust:


How to post a message with a quote and Rust code?
#2

I think the analogy with a C++ destructor is apt. In C++, when an object goes out of scope or is deleted, its destructor is run, and then the destructor of each of its members objects is run. This is similar to how drop works in Rust.


#3

In C++ a destructor must delete member objects that otherwise will leak. In languages with automatic memory management (AMM) it’s done by the AMM and not explicitly by the code. If a function can’t delete any object it can’t be called a destructor because destruction and deletion are synonyms in this context. That’s my point.

Finalizer sounds as a more accurate description name of drop. And it’s already used on other languages with AMM, like Java, C#, etc.


#4

Same in Rust. For example, a Box or Vec owns memory on the heap - if they don’t dealloc it in drop, it’s leaked. There’s no real difference between C++ and Rust here.


#5

It’s good to make the distinction between destructors and the Drop trait. The String doesn’t implement Drop, it just has a field with a Vec (which does implement Drop to implement part of what it needs to do when it is dropped, the rest is in its field RawVec). So the String has a destructor that drops its only field.


#6

Drop is a user hook into the destruction, not unlike a C++ destructor. The only big difference I see is in C++ you can call the destructor function explicitly and then continue using the object anyway - you can’t do that in (safe, at least) Rust.


#8

Same in Rust. For example, a Box or Vec owns memory on the heap - if they don’t dealloc it in drop, it’s leaked. There’s no real difference between C++ and Rust here.

Try to run the following code. The drop() function of the ObjectA doesn’t destruct the ‘b’ member, i.e. it doesn’t call to anything similar to the delete operator in C++. But the object in the ‘b’ member doesn’t leak.

struct ObjectA {
    b: Box<ObjectB>
}

struct ObjectB;

impl Drop for ObjectA {
    fn drop(&mut self) {
        println!("dropping ObjectA")
    }
}

impl Drop for ObjectB {
    fn drop(&mut self) {
        println!("dropping ObjectB")
    }
}

fn main() {
    {
        let _a: ObjectA = ObjectA { b: Box::new(ObjectB) };
    }
    println!("End")
}

#11

The Box destructor is executed automatically when the field is destructed. If the Box didn’t deallocate memory in its destructor then it would leak. But the default behavior in Rust is to destruct the fields automatically, whether you have an explicit Drop hook or not (a field can have its destructor suppressed by std::mem::forget or putting it into a ManuallyDrop union). If your struct acquires a resource that Rust won’t know how to destruct (eg raw ptr to heap storage) then it needs an explicit Drop that deallocates the heap storage.

But back to terminology - a destructor is something that runs when an object’s lifetime ends. RAII cases are more about doing something at the end of the scope, like releasing a mutex or deleting a file (or whatever you want to happen automatically at scope end). It doesn’t have to be memory management - it’s a concept/technique with broader usecases.


#12

In C++ terminology, a constructor does not necessarily creates any object, and a destructor does not necessarily deletes any object. A constructor is a function that is automatically invoked when an object is allocated (by other code) and a destructor is a function that is automatically invoked when an object is deleted (by other code). So, a constructor is used to initialize the constructed object, if there is such need, and a destructor is used to finalize that object, is there is such need. Rust uses such terminology. The word “finalizer” is never used when speaking of C++ (and of Rust), so there is no ambiguity.


#13

I’ve only ever seen “finalizer” used in managed runtimes (eg Java, .NET). I believe they use that term, rather than destructor, precisely because they don’t want to confuse that term with scope based destructors such as in C++ (or Rust nowadays).

It’s also worth mentioning that in “modern” C++ you rarely see new/delete invoked explicitly - most memory management is done with smart pointers. If a struct/class has smart pointer members, the compiler generated (ie default) destructors take care of calling destructors on them automatically.


#15

C++ destructors are called implicitly, just like in Rust. Don’t believe me? Try running this:

#include <iostream>

class Test2 {
    public:
    ~Test2() {
        std::cout << "Test2::~Test2()" << std::endl;
    }
};

class Test1 {
    Test2 test2;
    public:
    ~Test1() {
        std::cout << "Test1::~Test1()" << std::endl;
    }
};

int main() {
    Test1();
}

This is what’s outputted when I run it:

Test1::~Test1()
Test2::~Test2()

#16

Which is exactly the same in a C++ destructor when the ‘b’ member is a smart pointer.

Rust lacks the separate concept of delete operator with std::mem:drop taking its place.


#17

I’ve only ever seen “finalizer” used in managed runtimes (eg Java, .NET). I believe they use that term, rather than destructor, precisely because they don’t want to confuse that term with scope based destructors such as in C++ (or Rust nowadays).

You seems to be wrong. According to the following quote from a Wikipedia article about the Finalizer this term was invented in 1994 and was just not known by Bjarne Stroustrup during his C++ developing in 80’s:

The notion of finalization as a separate step in object destruction dates to Mongomery (1994),[12] by analogy with the earlier distinction of initialization in object construction in Martin & Odell (1992).[13] Literature prior to this point used “destruction” for this process, not distinguishing finalization and deallocation, and programming languages dating to this period, like C++ and Perl, use the term “destruction”. The terms “finalize” and “finalization” are also used in the influential book Design Patterns (1994).[a][14] The introduction of Java in 1995 contained finalize methods, which popularized the term and associated it with garbage collection, and languages from this point generally make this distinction and use the term “finalization”, particularly in the context of garbage collection.

And according to Mongomery:

As with object instantiation, design for object termination can benefit from implementation of two operations for each class - a finalize and a terminate operation. A finalize operation breaks associations with other objects, ensuring data structure integrity. Terminate frees up storage no longer used by classes that have been deleted. Object-oriented languages support the terminate operation, but usually the finalize operation needs to be provided to ensure referential integrity.

The terminate operation in C++ is the delete operator, isn’t it? A destructor in C++ may need to call the terminate operation thus it makes sense to be named destructor even today. But drop in Rust almost never needs to call a terminate operation, except a few special cases mentioned by you above.


#18

Hi notriddle,

Please change the Test2 test2; line in your example to something like Test2 *test2 = new Test2;


#19

I’m going by the colloquial use of these terms. The text you quoted even says that “finalize” was popularized by Java.

But it does in some cases - why is the frequency important?

RAII is a common use for destructors in C++, which don’t do anything with memory management. So I don’t see what distinction you’re really trying to prove here. It’s like we’re splitting hairs over something that doesn’t need it.


#20

The main difference between a finalizer in Java and a destructor in C++ is that a finalizer can do only a sub-set of things that a destructor can do. Of course you can use rudiments of automatic memory management in C++ (for example RAII) and in this case you need to implement only a finalization functionality in your destructor. But this is not a common case in C++ and you’re free to implement it differently. In Rust this is a common case and this is why I think it’s better to be named finalizer, like in Java. Special cases in Rust are just special cases and not a common case.

BTW why you use a term “method” (like in Java) instead of “member function” or just “function” in the error message about a direct call to drop? If you’re good with this Java term why you’re not with the finalizer one?

error[E0040]: explicit use of destructor method


#21

You’re missing the point. You can do that in Rust as well, it’s just not as common because Rust was designed from the start to use a unique_ptr-like Box type instead of manual (de)allocation.

C++ destructors and Rust drop impls do exactly the same work- they clean up any resources owned by the object that do not already have their own destructor/drop impl.


#22

I’d say the main difference is you don’t know when, if at all, the finalizer will run. The whole notion of a finalizer in Java is a flawed idea (well intentioned but flawed).

As I mentioned I think the most striking distance with C++ dctor and Rust is you can call the dctor manually in C++ and continue to use the object afterwards. The rest is the same.

Any function that takes self, &self or &mut self is called a method in Rust. There are also associated functions and free functions, so a distinction is drawn via names.


#23

I’ve never heard of such a distinction. Instead, they’re usually called explicit (for user written code) or implicit (or default) for compiler generated.


#24

You are right. I remembered badly. I have just withdrawn my post.