How can I implement Default for &Struct?
Asked Answered
C

1

6

After reading the rust book many times I think I'm starting to get the gist of lifetimes, but for me, an additional problem is the syntax we need to use to declare them. I find it is really counterintuitive.

I've simplified a silly code of mine onto this pair of structs (which one referencing the other).

#[derive(Debug, Default)]
pub struct TestStructA {
    pub byte_1: u8,
    pub byte_2: u8,
    pub vector: Vec<u8>,
}

impl<'a> Default for &'a TestStructA {
    fn default() -> &'a TestStructA {
        &TestStructA { byte_1: 10, byte_2: 20, vector: 'a vec![1, 2, 3] }
    }
}

#[derive(Debug, Default)]
pub struct TestStructB<'a> {
    pub test_array: &'a [u8],
    pub t_a: &'a TestStructA,
}

If you copy and paste this isolated code onto a main.rs file and compile it, then you get the following error:

error: expected `while`, `for`, `loop` or `{` after a label
  --> src/main.rs:10:59
   |
10 |         &TestStructA { byte_1: 10, byte_2: 20, vector: 'a vec![1, 2, 3] }
   |                                                           ^^^ expected `while`, `for`, `loop` or `{` after a label

error: labeled expression must be followed by `:`
  --> src/main.rs:10:59
   |
10 |         &TestStructA { byte_1: 10, byte_2: 20, vector: 'a vec![1, 2, 3] }
   |                                                        ---^^^^^^^^^^^^^
   |                                                        | |
   |                                                        | help: add `:` after the label
   |                                                        the label
   |
   = note: labels are used before loops and blocks, allowing e.g., `break 'label` to them

If I don't annotate the vector parameter lifetime, I get the expected: "check your lifetimes boy" (which makes sense).

error[E0515]: cannot return reference to temporary value
  --> src/main.rs:10:9
   |
10 |         &TestStructA { byte_1: 10, byte_2: 20, vector: vec![1, 2, 3] }
   |         ^-------------------------------------------------------------
   |         ||
   |         |temporary value created here
   |         returns a reference to data owned by the current function

Of course, if I choose a more drastic solution as removing the "vector" attribute from my struct, as all other attributes are scalars, the code compiles. But I need my struct to have non-scalar data structures of some kind. I suspect I need some kind of lifetime labeling for my vector inside the Default initializer, but I'm not sure what

By the way, I think my TestStructB is also properly lifetime annotated, but maybe not. Does it look correct?

Finally, I got here because the compiler said I needed to declare a Default initializer for the referenced (&) version of my original struct. This original struct was already happily used in my program, but never referenced. Whenever I started using it in a &referenced fashion, it was Rust claiming it needs its Default initializer. The thing is I still somewhat think I've done something wrong when using my referenced Struct, and that there is a proper way to use &referenced (and lifetime annotated) Structs without Rust complaining about their (non existing) default initializer.

EDIT: @JohnKugelman is more than right, I've come onto this "aberration" after the typical newbie fight with rust compiler trying to overcome the mainly cryptic messages it yields.

My simplified original was this one:

#[derive(Debug, Default)]
pub struct TestStructA {
    pub byte_1: u8,
    pub byte_2: u8,
    pub vector: Vec<u8>,
}

#[derive(Debug, Default)]
pub struct TestStructB<'a> {
    pub test_array: &'a [u8],
    pub t_a: &'a TestStructA,
}

With this code, the error I get is:

error[E0277]: the trait bound `&TestStructA: Default` is not satisfied
  --> src/main.rs:11:5
   |
11 |     pub t_a: &'a TestStructA,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `&TestStructA`
   |
   = help: the following implementations were found:
             <TestStructA as Default>
   = note: required by `std::default::Default::default`
   = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

Apparently rustc misled me into thinking I need a &Struct Default initializer? Can't say...

Clarissa answered 13/3, 2021 at 0:46 Comment(5)
impl<'a> Default for &'a TestStructA -- what led you to this? Let's trace backwards and figure out why you think you need it, because it's awfully sketchy. I'm thinking you should delete this whole block and then ask us about the original error that led you here.Unpolitic
"By the way, I think my TestStructB is also properly lifetime annotated, but maybe not. Does it look correct?" It's hard to say without knowing what TestStructB's purpose in life is. My instinct is to avoid using references/lifetimes inside structs if you don't need them. Your life will be easier if TestStructB owns its members: test_array: Vec<u8>, t_a: TestStructA.Unpolitic
I've edited my message to perform some trace back... Let's hope it doesn't become an edit mess. TestStructA will be some kind of configuration about some system. Some instance of this struct will live on some other module, and I want to pass a reference to a 2d render struct/module that will happily read its values for different render possibilities... Can't say if it's the proper way to do things "a la" RustClarissa
Do you need Default on TestStructB? Can you just remove it?Unpolitic
Unsure about that... In general the structs I've been using so far, Default trait made the initialization of their "normal" data structures really comfortable... I might refactor the one related with this specific struct, to see if I REALLY need it or not...Clarissa
S
9

Your unusual requirement arises because you're trying to implement Default for a structure TestStructB containing a reference. Most of the time, Default is used on types that have all owned values or can be empty in some way.

In order to implement Default for a reference &'a TestStructA, you would have to have some constant or leaked value of the type that the Default::default implementation can return a reference to — basically there needs to be an &'static TestStructA.

This is simple to do if the vector is empty because Vec::new() is a const fn, so we can construct a full compile-time-constant instance of TestStructA:

impl<'a> Default for &'a TestStructA {
    fn default() -> &'a TestStructA {
        static VALUE: TestStructA = TestStructA {
            byte_1: 10,
            byte_2: 20,
            vector: Vec::new()
        };
        &VALUE
    }
}

If you want to have some data in the vector, then you would have to use a lazy-initialization mechanism like once_cell to be able to execute vec! to allocate the vector's contents, then return a reference to it.

use once_cell::sync::Lazy;

static DEFAULT_A: Lazy<TestStructA> = Lazy::new(|| {
    TestStructA {byte_1: 10, byte_2: 20, vector: vec![1, 2, 3]}
});

impl<'a> Default for &'a TestStructA {
    fn default() -> &'a TestStructA {
        &DEFAULT_A
    }
}

If you were to do this, I'd recommend also implementing Default for TestStructA, because it is strange to have Default for the reference but not the owned value.

impl Default for TestStructA {
    fn default() -> TestStructA {
        DEFAULT_A.clone()
    }
}

(This only works if TestStructA also implements Clone, but it probably should. If it shouldn't, then instead put the struct literal inside the TestStructA::default method and have DEFAULT_A just be defined as Lazy::new(TestStructA::default).)

However, all these details are only necessary because

  1. You're implementing Default for TestStructB,
  2. which always contains a reference to a TestStructA.

You should consider whether TestStructB actually needs this reference in the default case — if t_a has type Option<&'a TestStructA>, for example, then it can just default to None. I don't have enough information to say whether this is appropriate for your application — make your own choice based on what the exact purpose of these structures is.

Sniggle answered 13/3, 2021 at 3:22 Comment(4)
Sadly for me, much of your response is more cryptic to me than I wanted... But you hit an intenresting nail: "Does my Struct need to live inside the other one as a reference?" I'd like it to, by instinct, but apparently in Rust this leads to cornercase situations... In my program this struct will hold some kind of "global configuration" about the screen. The owner of this struct will live somewhere else, and I will just send the reference for other people to use it... I wouldn't like to pass this struct as a reference in ALL functions of this other Struct, it feels "inefficient" to me...Clarissa
@Clarissa If the configuration truly is global, you shouldn't need a reference to it in TestStructB at all -- it's available everywhere by virtue of being global. If, on the other hand, it's not global and different TestStructBs might point to different TestStructAs, then you must pass the &TestStructA into TestStructB's constructor, so implementing Default is not possible (unless you can wrap it in an Option or something, as Kevin also observed). Implementing Default for a reference is just not really a meaningful thing to do in reasonable code.Bronny
I spoke about "global" and not literally global... I'm still in the phase of putting all the pieces into place regarding this program I'm creating, everything can sill be refactored somewhat easily. I'm not a friend of having truly global objects around, and I tried it holding this data inside a Struct and then passing instances of it here and there. But then my problem seems to be another: getting the gist of Struct vs Classes (which is what my mind is probably used to). I get nice topics fromm all this: "Structs better hold owned data", or else things will get steep.Clarissa
I mean, as of now I've abandoned the original intention of building a &Struct Default trait, and some refactor of my data structures has allowed me to push mi ideas forward in a different way. As I said somewhere in the question comments, I never had the intention of such a complicated duty, I just was poor Rust newbie-dodging the cryptical rustc errors being thrown onto me, until I reached that cul-de-sac. Time will put me into a wiser decision when dealing with Rust topics :)Clarissa

© 2022 - 2024 — McMap. All rights reserved.