Skip to content

Box::new flavors keep multiplying #90

Open

Description

@the8472

While working on rust-lang/rust#87777 I noticed that variants of Box::new keep growing. Currently they follow a pattern of {try?}_new_{uninit, zeroed, _}_{slice?}_{in?}.

I think moving this to a builder would make sense. I think most dimensions could be modeled as an enum, generic or associated type, the rest would still need separate build methods.

I imagine it ideally working something like Box::<[u8]>::new().zeroed().in(Alloc).slice(100).assume_init(). Of course most of the time one wouldn't want all of that noise. To keep the fallible stuff separate there would be a separate entry point into the builder via Box::try_new()....

I recall that there was discussion about in-place initialization with initializer functions (guaranteed RVO?), so at least the zeroed/uninit/value dimension might grow another member.

Activity

  1. thomcc commented on Aug 5, 2021

    @thomcc
    Member

    I'd want some guarantee that in practice this ends up with good codegen (at least on all of -O[3sz]).

    Allocation can be very hot, and my experience with using builders has been that they're best for non-performance intensive cases, and can generate bad code due to deficiencies in LLVM.

  2. the8472 commented on Aug 5, 2021

    @the8472
    MemberAuthor

    I think it could, for example RawVec already determines some of its behavior via an enum and it still optimizes fine on the higher opt levels. Also "guarantee" and "in practice" is a bit contradictory ;). But we could add codegen tests.

    https://github.com/rust-lang/rust/blob/61a941b8badbce727085c505068d72fa3e737f5b/library/alloc/src/raw_vec.rs#L22-L27

  3. thomcc commented on Aug 7, 2021

    @thomcc
    Member

    for example RawVec already determines some of its behavior via an enum and it still optimizes fine on the higher opt levels

    My experience is that RawVec is the source of some surprising behavior around optimizations (in particular it's often not optimized well IME), but I don't the problems I've seen are related to that enum at all.

    Also "guarantee" and "in practice" is a bit contradictory

    Ah sure, I meant assurance rather than guarantee, maybe. Codegen tests would certainly alleviate my worries, especially if the builder will likely grow more and more options over time.

    That said, if it's not something easy to set up for that part of the stdlib, it might be overkill.

  4. Kixunil commented on Feb 3, 2022

    @Kixunil

    I don't see how builder would help here, can you give an example?

  5. the8472 commented on Feb 3, 2022

    @the8472
    MemberAuthor

    Take

    new(value)
    new_in(value, allocator)
    new_uninit()
    new_uninit_in(allocator)
    new_zeroed()
    new_zeroed_in(allocator)

    That's 6 methods for the 2 dimensions

    • initialization stragegy: {zeroed, uninit, by-value}
    • allocator choice: {global, custom}

    With a builder it's 1 method to set an allocator (global being the default) and 2-3 methods to choose the initialization strategy.

    And then there are additional dimensions such as

    • fallibility
    • size (for [T])

    With a builder the dimensions add, without they multiply. E.g. today we don't have try_new_uninit_slice_in. Instead of adding yet another method to Box it would automatically fall out of the builder because it makes those aspects orthogonal.

  6. the8472 commented on Feb 3, 2022

    @the8472
    MemberAuthor

    Also, the solution doesn't have to be a builder. Parameterized functions could work too. My concern is the proliferation of methods, I'm not married to a particular solution.

  7. Kixunil commented on Feb 4, 2022

    @Kixunil

    The problem I see is they all affect the resulting type.

    • zeroed and uninit produce MaybeUninit<T>
    • by value produces T
    • allocator is in the type

    This would have to be represented in type state which will create a bunch of new types. I'm not convinced it's better. That's also why I asked for specific example because maybe I'm missing some solution and seeing it in an example would be the best way to show me and even prove it's possible.

  8. the8472 commented on Feb 4, 2022

    @the8472
    MemberAuthor

    Oh so you were asking for an example implementation? I thought you were asking for an example of method properties that could be orthogonalized.

    This would have to be represented in type state

    Box already is <T, A>, I think a builder wouldn't be much more complicated than that, except maybe needing associated types instead of generics. But I haven't worked out the details because, as I said before, that I don't advocate for a specific solution, I mainly wanted to highlight the issue.

  9. sollyucko commented on Feb 4, 2022

    @sollyucko

    What about something like this? The keys to reducing the number of types are requiring a specific ordering and taking advantage of generics. Also, it shouldn't be too difficult to add new options, especially at the end.

    Pseudo-Rust:

    fn Box<T: ? Sized>::builder() -> BoxBuilder1<T>;
    fn Box<T: ? Sized, A: AllocRef>::builder_in(allocator: A) -> BoxBuilder1<T, A>;
    
    fn BoxBuilder1<T, A: AllocRef>::init(value: T) -> BoxBuilder2<T, A>;
    fn BoxBuilder1<T, A: AllocRef>::uninit() -> BoxBuilder2<MaybeUninit<T>, A>;
    fn BoxBuilder1<T, A: AllocRef>::zeroed() -> BoxBuilder2<MaybeUninit<T>, A>;
    fn BoxBuilder1<[T], A: AllocRef>::init(value: impl Unsize<[T]>) -> BoxBuilder2<[T], A>;
    fn BoxBuilder1<[T], A: AllocRef>::init_copied(value: &[T]) -> BoxBuilder2<[T], A>;
    fn BoxBuilder1<[T], A: AllocRef>::uninit(len: usize) -> BoxBuilder2<[MaybeUninit<T>], A>;
    fn BoxBuilder1<[T], A: AllocRef>::zeroed(len: usize) -> BoxBuilder2<[MaybeUninit<T>], A>;
    
    fn BoxBuilder2<T: ?Sized, A: AllocRef>::try_build() -> Result<Box<T, A>, A::Err>;
    fn BoxBuilder2<T: ?Sized, A: AllocRef>::build() -> Box<T, A>;
  10. Kixunil commented on Feb 4, 2022

    @Kixunil

    @sollyucko I think builder 1 and 2 should be reversed to enable zero-copy initialization. However it could cause problems around inference or so and slices would be problematic.

  11. Wodann commented on Feb 9, 2022

    @Wodann

    Of course most of the time one wouldn't want all of that noise.

    Is your goal to reduce noise for the user of the API? I feel like there are generally two target audiences for APIs:

    1. power users - that are trying to get maximum performance.
    2. casual users - that are looking for the most convenient way to get the job done.

    To serve the needs of 1. you need to design low-level APIs that directly tie into the underlaying system. For 2. you can create wrapper/helper APIs that add simplifying logic.

    Our low-level API should already be the Allocator API, which is reused in Box, RawVec, etc. It seems like we're currently bleeding that API to other classes built on top of it, making the lives of **2. ** harder. It seems like it'd suffice to just use the Allocator API when someone wants to get raw performance and directly use the result to create a box.

    Pseudocode:

    trait Allocator {
      fn try_allocate_*<T>(&self) -> Result<NonNull<T>, AllocError>;
    }
    
    impl<T, A: Allocator> Box<T, A> {
      fn from_raw(ptr: NonNull<T>, allocator: A) -> Box<T, A> {
        Box(ptr, allocator)
      } 
    }
    
    fn usage(allocator: &Allocator) {
      let foo = allocator.try_allocate_*::<T>().map(|a| Box::from_raw(a, allocator).expect("Failed to allocate.");
    }

    If there are concerns about safety (mismatched allocation & allocator), we could (re-)consider using a "result type":

    struct Allocation<T, A: Allocator> {
      ptr: NonNull<T>,
      allocator: A,
    }

    allowing:

    impl<T, A: Allocator> From<Allocation<T, A> for Box<T, A> {
      fn from(allocation: Allocation<T, A>) -> Box<T, A> {
        Box(allocation.ptr, allocation.allocator)
      }
    }
  12. berkus commented on Feb 17, 2022

    @berkus

    Note to self: probably where context and capabilities could be super helpful.

  13. the8472 commented on Apr 29, 2022

    @the8472
    MemberAuthor

    An additional advantage of a builder is that it could build Box, Rc, Arc and possibly other types without multiplying all of the methods. Essentially the outpout container is yet another dimension.

  14. tgross35 commented on Oct 31, 2023

    @tgross35
    Member

    Do you have an idea what the builder would look like here? I've played with it a bit and can't figure out a good way to handle all the conversions among types

    If this is looking like a way forward, it would be nice to have something in nightly for a while

  15. Jules-Bertholet commented on Nov 22, 2023

    @Jules-Bertholet

    I recall that there was discussion about in-place initialization with initializer functions (guaranteed RVO?)

    rust-lang/rfcs#2884

  16. tgross35 commented on Mar 8, 2024

    @tgross35
    Member

    I might have something workable-ish here

    #[derive(Debug, Clone)]
    pub struct BoxBuilder<T: ?Sized, D: ?Sized = Init<T>, A = Global, F = Infallible> {
        zeroed: bool,
        alloc: A,
        slice_len: usize,
        falliblity: F,
        ty: PhantomData<T>,
        data: D,
    }
    
    fn main() {
        let builder = BoxBuilder::new(1234u32);
        dbg!(&builder);
    
        let v = builder.clone().build_box();
        assert_eq!(v, Box::new(1234u32));
    
        let v = builder.clone().build_rc();
        assert_eq!(v, Rc::new(1234u32));
    
        let v = builder.clone().build_arc();
        assert_eq!(v, Arc::new(1234u32));
    
        let v = builder.clone().fallible().build_box();
        assert_eq!(v, Ok(Box::new(1234u32)));
    
        let v = BoxBuilder::new_uninit().zeroed().build_box();
        unsafe { assert_eq!(v.assume_init(), Box::new(0)) };
    }

    Full long code https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=1e4ee1bb361af4f6b0807ccc4313508b

    It currently can't handle unsized types so slices do not work, but that is probably fixable. Generics look pretty ugly, maybe some could be combined.

  17. the8472 commented on Jul 6, 2024

    @the8472
    MemberAuthor

    rust-lang/rust#127415 will increase the counter by 2.

  18. AljoschaMeyer commented on Jul 6, 2024

    @AljoschaMeyer

    You are welcome 🙃

    In more seriousness, a principled approach like the one suggested in this issue would have ensured that the oversight of those particular combinations had never occurred in the first place. Which is definitely a plus for what is being proposed here.

  19. 8573 commented on Jul 8, 2024

    @8573

    I'm surprised by the builder's requiring that a payload be provided first and being configured only after that, which is the opposite of what I would expect from the builder pattern.

    That up-front payload also seems confusingly pointless in the second-to-last example, where it's replaced with zero.

  20. Skgland commented on Jul 8, 2024

    @Skgland

    I'm surprised by the builder's requiring that a payload be provided first and being configured only after that, which is the opposite of what I would expect from the builder pattern.

    That up-front payload also seems confusingly pointless in the second-to-last example, where it's replaced with zero.

    So it should be more like this which also supports unsized payload.

    Edit: v2 now init/uninit/zeroed , sized/unsized is back in the builder

    v3 now Box/Rc/Arc is also selected via the builder, only a single build function rather than build_box, build_rc, build_arc

    v4 like v3 but with a custom ThinBox MyThinBox SmartPointer type that supports ?Sized

  21. tgross35 commented on Jul 8, 2024

    @tgross35
    Member

    I'm surprised by the builder's requiring that a payload be provided first and being configured only after that, which is the opposite of what I would expect from the builder pattern.

    That up-front payload also seems confusingly pointless in the second-to-last example, where it's replaced with zero.

    Referring to my example? There is a new_uninit() that doesn't take a value, I don't know why I didn't just put that in the example in the comments (updated it).

    Edit: you're right about new, the payload should be the last thing so you can reuse the base.

  22. the8472 commented on Feb 25, 2025

    @the8472
    MemberAuthor

    Another potential use-case: rust-lang/libs-team#544

    This could for example be solved by a builder function that takes a closure which in turn takes a pointer to be initialized.

    Though as that ACP says ideally it would be solved through MaybeUninit too.

  23. kornelski commented on Nov 19, 2025

    @kornelski
  24. added this to the Post-MVP milestone on Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions