Some Questions on The Destructuring of Tuple Structures

// `Color` doesn't implement `Copy` trait here.
struct Color(u8, u8, u8);

fn main() {
    let color = Color(32, 32, 32);
    let Color(r, g, b) = color;
    // rustc doesn't complain about this:
    println!("color: ({}, {}, {})", color.0, color.1, color.2);
}

What happened in the second let statement? Does the variables r, g, and b move the ownership of color in this example? Or it will move or copy the value of the members of color depending on the members' own rules? In other words, does Rust regard the Color(r, g, b) as a single object applying the rules of moving and copying semantics of Color or only applying the rules members owned? — in my test, I noticed that I still could use color after destructuring without any compilation failure, whereas Color doesn't implement Copy trait, which is weird.

I question this here because I noticed that color: Color(32, 32, 32) is regarded as a single value here, but Color(r, g, b) seems to be regarded as three separated variables, and I couldn't use (r, g, b) to destruct the c as an error occurred in compilation: mismatched types, which makes me confused — if Rust regards r, g, and b as three separated and independent variables, then why I must specify the structure which is similar to a construction explicitly rather than using a tuple to destruct the tuple structures directly? The former one makes me unsure that whether the = operation of Color(r, g, b) is applied by the ownership rules of the Color itself or its members — in the example, Color cannot Copy but u8 could.

1 Like

No, because = color; there is using it as a place rather than a value, so the u8s are copied, rather than moving the whole Color.

If instead you try this, to force a move

let Color(r, g, b) = { color };

then you'll get the error you probably expected:

error[E0382]: borrow of moved value: `color`
 --> src/main.rs:8:55
  |
5 |     let color = Color(32, 32, 32);
  |         ----- move occurs because `color` has type `Color`, which does not implement the `Copy` trait
6 |     let Color(r, g, b) = { color };
  |                            ----- value moved here
7 |     // rustc doesn't complain about this:
8 |     println!("color: ({}, {}, {})", color.0, color.1, color.2);
  |                                                       ^^^^^^^ value borrowed here after move
  |
note: if `Color` implemented `Clone`, you could clone the value
 --> src/main.rs:2:1
  |
2 | struct Color(u8, u8, u8);
  | ^^^^^^^^^^^^ consider implementing `Clone` for this type
...
6 |     let Color(r, g, b) = { color };
  |                            ----- you could clone this value
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1549620a0af21ee565c0fc684dcd6696

4 Likes

@scottmcm already gave the key fact that the right side of let is used as a place, but here is some more information on what that means.

Whenever you write let <pat> = <expr> or match <expr> { <pat> => ..., whether any parts of the value of the <expr> are moved, copied, borrowed, or left alone is entirely up to what <pat> is. For example, suppose I define a struct:

struct Person {
    name: String,
    favorite_number: u128,
}

let person = Person {
    name: "kpreid",
    favorite_number: 65536,
};

then if I match it like this:

let Person { favorite_number, .. } = person;

this pattern binds only one field of person, and that field’s type, u128, is Copy, so person has not been moved-out-of and can still be used. On the other hand, if I match it like this:

let Person { name, .. } = person;

then the String has been moved out of person, so person has been moved-out-of and can no longer by used as a whole value; the only things you can do with person at this point are to

  • access its favorite_number field, or
  • restore it to normal functionality by putting a String back into person.name.

(In general, structs that are stored in variables act a lot like each one of their fields is a separate variable.)

You can also bind a field by reference, so as not to move out of it:

let Person { ref name, .. } = person;

This syntax is rarely used today because “match ergonomics” or “default binding modes” allow you to get the same effect, usually more conveniently for multiple fields, by matching on a reference:

let Person { name, .. } = &person;

This is actually syntactic sugar for a pattern that explicitly reaches into the reference to reborrow from it:

let &Person { ref name, .. } = &person;

Yes. if the pattern match let Color(r, g, b) = ... is permitted,[1] then whether the result is a move or copy depends on the type of each matched field, and not on what properties Color as a whole has.

Treatment of r, g, and b are “three separated and independent variables” has nothing to do with how the type Color is defined other than that it is a struct. This treatment applies to all structs, not only tuple structs.

Also, it sounds like you are under the impression that tuple structs might be interchangeable with tuples in some way. The only thing that is special about tuple structs is that they have syntax involving parentheses.[2] Everything else is the same for tuple structs and braced structs — and all of them are distinct from tuples, which are their own family of non-struct types.

That is, this:

struct Color(u8, u8, u8);
let color = Color(32, 32, 32);
let Color(r, g, b) = color;

has all the same behaviors as this:

struct Color { r: u8, g: u8, b: u8}
let color = Color { r: 32, g: 32, b: 32 };
let Color { r, g, b } = color;

The choice of whether to use a tuple struct or a regular struct is purely aesthetics.


  1. Factors include field privacy and the presence of a Drop impl. ↩︎

  2. and fields with numbers rather than names ↩︎

8 Likes

Can you elaborate on the difference to plain old tuples in this context? It appears to me, that they behave the same way as tuple structs in terms of pattern matching.

fn main() {
    let person_tuple = ("schard".to_string(), 42);
    let (name, ..) = person_tuple;
    dbg!(name);
    dbg!(person_tuple); // Error, moved.
}
fn main() {
    let person_tuple = ("schard".to_string(), 42);
    let (.., favorite_number) = person_tuple;
    dbg!(favorite_number);
    dbg!(person_tuple); // Ok, cos copy
}

That one is easy: “if Rust regards” is the premise that's flaved. Rust is not an entity. It's not even pretend-entity like LLM. Rust follows rules.

And it was decided that adding a shortcut that would allow you to destructure struct into tuple is simply not worth it. That's it.

Rust complier doesn't “look” on your program and doesn't “understand” it.

Rather the Rust developers look on bunch of programs, understand (or don't understand them), then write simple rules that are applied.

And word “simple” the key. Do not look on each program separately and apply “common sense” to it. That's not how compilers work. Rather it's beneficial to think about different programs and imagine single rule that would cover all of them.

Consider trivial modification of your program:

// `Color` doesn't implement `Copy` trait here.
struct Color(u8, u8, u8);

fn main() {
    let color;
    // Some time later, could be 100 lines.
    color = Color(32, 32, 32);
    let Color(r, g, b) = color;
    // rustc doesn't complain about this:
    println!("color: ({}, {}, {})", color.0, color.1, color.2);
}

How does color on the left side of assignment makes any sense? If that would have been value then how can you assign to it? Heck, if that's a value then how something like my_ref = &color; may have any meaning, of color is value, not place?

No, for the rule to be simple color have to be a place, not a value. And then it may be processed in a different way: you can move out of it with { color }, you can assign to it with color =, you can take a reference to that place with &coloir.

Same with destructuring: for the rules to be simple compiler have to look on the left part, before = independently from what's on the right. And then “shapes” have to match thus it doesn't detructure struct into tuple.

Note that Rust is not Go, it doesn't try to make language “simple” by shoving all the complexity into your head and does have some extremely complicated and convoluted rules… where it's worth it.

There's a language strangeness budget and wasting it on something that's not often needed is bad idea. And making it possible to destructure struct into tuple was just not considered important enough to support.

C++, e.g., did the opposite decision: in C++ the only way you may destructure something is with tuple-style syntax, whether it's struct, tuple, or vector, syntax is still tuple-like.

Rust picked the other way and tries to make destructuring like an opposite of type declaration.

It's all arbitrary, to some extent.

1 Like

I now think I misread @rovol’s post, but initially it seemed to me like it was claiming that, in some way, tuple structs were made out of tuples, which is not true, so I was responding to that.

I agree with you that tuples and tuple structs behave the same in pattern matching. I would further point out that braced structs also do, with the caveat that the tuple-style matching syntax is not available to braced structs, but you can still get all the same matching outcomes.

1 Like