What are the differences between the multiple ways to create zero-sized structs?
Asked Answered
F

2

23

I found four different ways to create a struct with no data:

  • struct A{} // empty struct / empty braced struct
    
  • struct B(); // empty tuple struct
    
  • struct C(()); // unit-valued tuple struct
    
  • struct D; // unit struct
    

(I'm leaving arbitrarily nested tuples that contain only ()s and single-variant enum declarations out of the question, as I understand why those shouldn't be used).

What are the differences between these four declarations? Would I use them for specific purposes, or are they interchangeable?

The book and the reference were surprisingly unhelpful. I did find this accepted RFC (clarified_adt_kinds) which goes into the differences a bit, namely that the unit struct also declares a constant value D and that the tuple structs also declare constructors B() and C(_: ()). However it doesn't offer a design guideline on why to use which.

My guess would be that when I export them with pub, there are differences in which kinds can actually be constructed outside of my module, but I found no conclusive documentation about that.

Flathead answered 3/5, 2018 at 19:31 Comment(12)
There is no mention of unit type preferences in the Rust API guidelines. I'm afraid that there is no objective answer to this. :-( You may wish to send this over to users.rust-lang.org or similar venues.Polytypic
@Polytypic So you are saying that there is no difference, and it comes down to preference? (In that case, unit structs having dedicated syntax for this purpose makes D the most obvious choice)Flathead
I am implying that it's mostly case dependant. The most obvious choice isn't always the best one. Composition of unit values also make a unit value. It also depends on whether creating them from outside the crate is allowed in your public API.Polytypic
@Polytypic That sounds like an answer if some of them allow creating values from outside the crate and others don't. That's exactly the part I couldn't figure out.Flathead
You ask two questions: "What are the differences?" and "When would I use them?" One of these is on-topic on SO; the other is not. If you stick to the on-topic one, you'll probably get an answer.Sunn
@trentcl I didn't mean to ask "Which should I prefer if there are no differences?", sorry - that's clearly offtopic indeed. Edited. Assuming there are differences, I was looking for the scenarios where those differences matter. I hope it's not considered too broad.Flathead
I can think of a few more cases off the top of my head. struct A { _marker: PhantomData } is also a zero-sized struct, struct A<T> { t: T } is a zero-sized struct if T is zero-sized, enum A {} (a no-variant enum) is also zero-sized, ...Denudation
@MatthieuM. PhantomData has a clear purpose, I know when to use that. A no-variant enum is like ! (void) not () (unit), no value can exist at all.Flathead
@Bergi: Good point on the no-variant :)Denudation
since "unit type" is just a tuple with no fields, shouldn't the "unit struct" be rather struct E(); than struct E; ? That would make more sense.Kelle
@Kelle The reference calls struct E; a "unit-like struct".Flathead
@Flathead yes, I know, I was wondering why is thatKelle
S
13

There are only two functional differences between these four definitions (and a fifth possibility I'll mention in a minute):

  1. Syntax (the most obvious). mcarton's answer goes into more detail.
  2. When the struct is marked pub, whether its constructor (also called struct literal syntax) is usable outside the module it's defined in.

The only one of your examples that is not directly constructible from outside the current module is C. If you try to do this, you will get an error:

mod stuff {
    pub struct C(());
}
let _c = stuff::C(());  // error[E0603]: tuple struct `C` is private

This happens because the field is not marked pub; if you declare C as pub struct C(pub ()), the error goes away.

There's another possibility you didn't mention that gives a marginally more descriptive error message: a normal struct, with a zero-sized non-pub member.

mod stuff {
    pub struct E {
        _dummy: (),
    }
}
let _e = stuff::E { _dummy: () };  // error[E0451]: field `_dummy` of struct `main::stuff::E` is private

(Again, you can make the _dummy field available outside of the module by declaring it with pub.)

Since E's constructor is only usable inside the stuff module, stuff has exclusive control over when and how values of E are created. Many structs in the standard library take advantage of this, like Box (to take an obvious example). Zero-sized types work in exactly the same way; in fact, from outside the module it's defined in, the only way you would know that an opaque type is zero-sized is by calling mem::size_of.

See also

Sunn answered 4/5, 2018 at 16:0 Comment(4)
Thanks, I think this is what I was looking for. Is there any way to make the constructor of a struct public?Flathead
It makes sense that a Box should not be manually constructed, because it needs to place data on the heap. But what is a practical use case of a public zero-sized struct with private constructor?Carrington
@kazemakase I knew someone would ask, but I didn't have a use case in mind. The book suggests using such structs as pointer targets in FFI (although they use [u8; 0] instead of () for unclear reasons). I can also imagine using a struct like this to control access to a global resource, through some combination of compile time and runtime enforcement... something like lazy_static does but with extra requirements. Maybe you can think of something better?Sunn
@Flathead Sure, by marking all its members pub. I have added this information to the answer.Sunn
B
10
struct D; // unit struct

This is the usual way for people to write a zero-sized struct.

struct A{} // empty struct / empty braced struct
struct B(); // empty tuple struct

These are just special cases of basic struct and tuple struct which happen to have no parameters. RFC 1506 explains the rational to allow those (they didn't used to):

Permit tuple structs and tuple variants with 0 fields. This restriction is artificial and can be lifted trivially. Macro writers dealing with tuple structs/variants will be happy to get rid of this one special case.

As such, they could easily be generated by macros, but people will rarely write those on their own.

struct C(()); // unit-valued tuple struct

This is another special case of tuple struct. In Rust, () is a type just like any other type, so struct C(()); isn't much different from struct E(u32);. While the type itself isn't very useful, forbidding it would make yet another special case that would need to be handled in macros or generics (struct F<T>(T) can of course be instantiated as F<()>).

Note that there are many other ways to have empty types in Rust. Eg. it is possible to have a function return Result<(), !> to indicate that it doesn't produce a value, and cannot fail. While you might think that returning () in that case would be better, you might have to do that if you implement a trait that dictates you to return Result<T, E> but lets you choose T = () and E = !.

Boozy answered 4/5, 2018 at 8:17 Comment(2)
So most of these exist just because generic code could end up with them, not because they have different functionality? I guess I was confused because here struct C(()); was used directly while in other places in the same codebase struct A; was used.Flathead
@trentcl provides another nice use case in his answer.Boozy

© 2022 - 2024 — McMap. All rights reserved.