Buggy generics or a crutch in compiler/std?

I was playing around with generics and found some strange behavior related to struct size and
type_name function behavior.

Code:

trait SampleTrait<V> {
    fn foo<T: Sized>(&self, _p : T) {
        println!("{}", std::any::type_name::<T>());
        println!("{}", std::any::type_name::<V>());

        println!("{}", std::mem::size_of::<T>());
        println!("{}", std::mem::size_of::<V>());

        println!("{}", std::any::type_name::<&&&&&&&&&&&V>());
        println!("{}", std::mem::size_of::<&&&&&&&&&&&V>());
    }
}

struct Test {
    s : String
}

impl SampleTrait<Test> for Test {

}

fn main() {
    let test = Test{
        s: String::from("42")
    };
    test.foo(&test);
}

Output (testp is a project name here):

&testp::Test
testp::Test
8
24
&&&&&&&&&&&testp::Test
8

Seems like & might be written infinite times. Also I do not understand why the size of the Test struct differ.

The reference is smaller because references are just pointers, and pointers take up 8 bytes on most modern machines. The size_of method does not include memory that it points at when computing its size.

As for & written multiple times, well, yes. A reference to something is a distinct type from the thing itself, and you can make a reference to any type. Since references are types themselves, you can have a reference to a reference.

3 Likes

Thank you for the quick reply. I'm new for Rust community, so the part with a pointer size was a bit silly for sure...

This behavior with refferences reminds of C++ rvalues, but it kinda strange anyways. At least it might be compressed to a single number or smth similar.

While "how much memory is 'reachable' from this struct" is sometimes a useful thing to know, there's no way for the compiler (or standard library, or anything else) to tell you that in the general case.

  • It's a dynamic (runtime) property, not a static (compile time) property of a type
  • It can not be understood without special casing every struct
    • E.g. Vec stores a pointer and a capacity value, not a slice with a length the compiler understands
    • The compiler also doesn't understand that some of that memory is uninitialized, so it wouldn't know to not recursively check that portion memory
    • (Incidentally, String is a Vec<u8> underneath)
  • There are other ambiguous, undecidable, and painful-to-deal-with scenarios you can create even with the standard and built-in types
    • E.g. references to stack locations or static memory
    • E.g. reference-counted types making the same memory reachable through multiple paths
    • E.g. untagged unions
    • E.g. type-erased object (dyn Trait)
  • Even if some function special-cased every composition of standard library type to the extent possible, it wouldn't help with custom types that do similar things

There are ways to track memory usage at a higher level.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.