Geschiedenis. Vroegere Rust vereiste dat alle typen een statisch bekende grootte hadden om stackallocatie en efficiënte waarde-semantiek te garanderen. Toen dynamisch sized types (DSTs) zoals slices [T] en trait-objecten dyn Trait werden geïntroduceerd om flexibele datastructuren te ondersteunen, moest de taal een mechanisme hebben om te onderscheiden tussen sized en potentieel unsized generieke parameters zonder bestaande code te breken. De ?Sized syntaxis werd aangenomen als een "verruimde" beperking, waarmee generics expliciet konden afzien van de standaard Sized eis, terwijl de ergonomische standaard voor de meeste gebruiksgevallen die geen unsized data omvatten behouden bleef.
Het Probleem. De impliciete T: **Sized** beperking creëert een fundamentele spanning: deze maakt waarde-manipulatie en compile-tijd geheugencalculaties mogelijk, maar voorkomt dat functies dyn Trait of slice-types direct accepteerden zonder indirectie. Deze beperking dwingt ontwikkelaars om Box of referenties te gebruiken, zelfs wanneer eigendomssyntaxis gewenst is, wat API's compliceert die zowel statische als dynamische polymorfisme willen ondersteunen. Zonder ?Sized kan generieke code niet abstraheren over zowel concrete types als runtime polymorfe objecten, wat leidt tot gedwongen heapallocatie of gedupliceerde interfaces voor sized en unsized varianten.
De Oplossing. De compiler lost dit op door te handhaven dat types die door ?Sized zijn begrensd, alleen kunnen worden benaderd via fat pointers—composietwaarden die een datapointer en runtime metadata bevatten (lengte voor slices, vtable voor trait-objecten). Wanneer een generiek T: **?Sized** specificeert, verbiedt de compiler operaties die bekende groottes vereisen, zoals std::mem::size_of::<T>() of het verplaatsen van waarden per waarde, waardoor ervoor gezorgd wordt dat alle geheugengebreken berekenbaar blijven tijdens de compileertijd. Dit ontwerp maakt zero-cost abstracties mogelijk waarbij sized types dunne pointers gebruiken en unsized types dikke pointers gebruiken, terwijl het type systeem de onderscheiding transparant hanteert.
Een systemenmonitoringsbibliotheek moest fouten loggen die zowel kleine, stack-geallocate foutcodes als grote, dynamisch opgemaakte foutberichten konden zijn die dyn **Display** implementeerden. Het initiële API-ontwerp met fn log<T: **Display**>(error: T) wees trait-objecten af omdat de impliciete Sized beperking verhinderde dat dyn Display voldeed aan de beperking, wat een aanzienlijke ergonomische hindernis voor dynamische foutafhandeling creëerde.
De eerste benadering die overwogen werd, was het verplichten van Box<dyn **Display**> voor alle fouttypes, waardoor zelfs eenvoudige u32 foutcodes in heapallocaties werden omgezet. Voordelen: Unified de API-oppervlakte en stond eigendom van dynamische fouten toe zonder complexe generics. Nadelen: Introduceerde allocator-afhankelijkheden die niet geschikt zijn voor embedded targets en voegde meetbare vertraging toe aan drukke paden die eenvoudige, statische fouten behandelden.
De tweede optie hield in dat er twee aparte loggingmethoden werden onderhouden: een voor generieke T: **Display** sized types en een specifiek voor &dyn **Display**. Voordelen: Vermeden heapallocatie voor sized types en ondersteunden correct dynamische dispatch voor complexe fouten. Nadelen: Vereiste significante codeduplicatie, compliceerde de publieke API-documentatie en dwong oproepers om de juiste methode te kiezen op basis van voorgaande kennis van de grootte van het type.
Het team koos een derde benadering met fn log<T: **?Sized** + **Display**>(error: &T), dat verwijzingen naar zowel sized als unsized types accepteerde. Deze oplossing werd gekozen omdat het een enkel, coherent API-ingangspunt behield, no-std omgevingen ondersteunde door verplichte boxing te vermijden, en nul runtime overhead oplegde vergeleken met de dual-methodenbenadering. De generieke implementatie compileerde tot identieke machinecode voor sized types als de oorspronkelijke monomorfische versie, terwijl correct omging met trait-objecten via vtable dispatch.
De resulterende crate werd met succes uitgerold over microcontrollers en servers, die miljoenen heterogene foutgebeurtenissen verwerkten zonder allocatie overhead. De uniforme interface stelde ontwikkelaars in staat om zowel &ConcreteError als &dyn Error naadloos door te geven, wat aantoont dat ?Sized echt kosteloze polymorfisme mogelijk maakt over diverse implementatiedoelen.
Waarom kan een functie geen waarde van type T retourneren waar T: **?Sized**?
Functies die waarden retourneren moeten die waarden in registers of op de stack plaatsen, wat een compile-tijd bekende grootte vereist om de juiste aanroepconventiecode te genereren en de juiste stackruimte te reserveren. Aangezien ?Sized typen zoals [i32] of dyn **Debug** runtime-bepaalde groottes hebben, kan de compiler de vaste-grootte return instructiesequenties die nodig zijn voor de ABI niet genereren. Alleen pointertypes (Box<T>, &T) hebben statisch bekende groottes (usize of fat pointer breedte), waardoor ze de enige legale retourtypen voor unsized data zijn, waardoor de ?Sized generics fundamenteel worden beperkt tot "view" types in plaats van "value" types die per waarde kunnen worden verplaatst.
Hoe interageert **?Sized** met de coherentieregels met betrekking tot trait-implementaties voor referenties?
Bij het implementeren van traits voor &T waar T: **?Sized**, geldt de implementatie automatisch voor fat pointers (zoals &[i32] of &dyn Trait) omdat dit eenvoudigweg referenties zijn naar ?Sized types. Kandidaten missen vaak dat impl Trait voor &T waar T: **?Sized** zowel dunne als dikke pointers dekt, terwijl impl Trait voor T waar T: **Sized** dat niet doet. Dit onderscheid is cruciaal voor het definiëren van blanket-implementaties die werken met zowel sized data als trait-objecten, om coherentie in de typehiërarchie te waarborgen zonder overlappende implementaties die de orphan-regels van Rust zouden schenden.
Wat onderscheidt de geheugenaanduiding van **Box<dyn Trait>** van **&dyn Trait** naast eigendomssemantiek?
Terwijl beide fat pointers gebruiken (pointer + vtable), bezit **Box<dyn Trait>** de allocatie en slaat specifiek de vtable-pointer op voor deontologische doeleinden, terwijl **&dyn Trait** enkel de data observeert. Het is cruciaal dat Box<T> waar T: **?Sized** vereist dat de allocator dynamisch sized deallocatie afhandelt met behulp van de grootte die in de vtable is opgeslagen, terwijl referenties geen dergelijke verantwoordelijkheid hebben. Beginners over het hoofd zien vaak dat Box heapallocatie van unsized types mogelijk maakt die niet op de stack kunnen bestaan, terwijl referenties eenvoudigweg bestaande geheugen lenen, waardoor Box essentieel is voor het retourneren van eigendom van unsized data uit functies.