Rust 1.51 makes array lengths generic and separates build features
Minimal const generics let library declarations preserve array lengths. Cargo's second resolver prevents host-only feature requirements from unnecessarily affecting target dependencies.
An array's length has always been part of its Rust type, but library authors could not previously abstract over that length in the same way they abstracted over an element type. Rust 1.51 removes that asymmetry for a deliberately restricted set of constant parameters. A function or structure can now carry an array length through its declaration without requiring a separate implementation for each size.
The March 25 release announcement calls this the const-generics MVP. It also introduces Cargo's second feature resolver. These changes address different systems, but both preserve distinctions that matter to correctness. An array length stays in the type relationship, while a build dependency's host-side features need not determine a target dependency's features.
I am interested in Rust where a declaration can express a constraint that would otherwise survive only as a convention. Const generics make array-size relationships available to ordinary library code. The useful question is which relationships this first stable version can actually express.
A constant becomes a generic parameter
The release's example combines a type parameter with a length parameter of type usize. A structure can use that parameter in its array field. Different instantiations select different lengths, while the declaration remains one definition.
struct Pair<T, const N: usize> {
left: [T; N],
right: [T; N],
}
This example follows the pattern in the February beta explanation. Both fields have the same element type and length. Constructing the type requires values that satisfy those field types. There is no separate runtime length field whose agreement with the arrays must be maintained manually.
The distinction from a slice is the information expressed by the type. A slice can describe a borrowed sequence with a runtime length. This structure describes two arrays whose common length participates in the generic instantiation. Which representation is appropriate depends on whether the relationship belongs in the API's type contract. A generic length also avoids selecting one arbitrary supported size as the library's special case. The caller chooses the instantiation, and both fields follow that choice. The declaration expresses their agreement without duplicating the structure for another length.
The beta explanation notes the history behind this gap. Standard library array methods were once limited to arrays of length at most 32 because implementations could not be written generically over arbitrary lengths. Rust 1.47 removed that library restriction using const generics internally. Rust 1.51 now makes the stable language capability available to library authors more broadly.
This chronology prevents confusing library support with public language stabilization. Users could benefit from more general standard library implementations before they could write equivalent generic declarations on stable Rust. The new release changes what downstream code can express itself.
The MVP has a defined expression boundary
Supported constant-parameter types include signed and unsigned integers, including isize and usize, as well as bool and char. User-defined structure and enum values remain outside the stable subset. The release is therefore broader than array lengths alone, but narrower than arbitrary compile-time values.
The beta article gives three accepted forms for constant arguments. A standalone constant parameter is valid, a literal is valid, and a concrete constant expression without generic parameters is valid when enclosed in braces. Expressions that compute with generic parameters remain restricted.
For example, carrying N directly into an array type works. Computing a new length as N plus one does not become stable merely because the expression is simple to a human reader. An expression involving size_of of a generic T has the same problem. Its value depends on a generic parameter, so it is outside the concrete-expression case.
The implementation difficulty includes deciding when unevaluated expressions should count as equal. The beta explanation points out that two occurrences of N plus one are separate entities in the compiler. Establishing that they denote the same constant requires reasoning that simple substitution does not provide automatically.
Errors during evaluation pose another problem. An operation describing an array of length N minus one must explain what happens when N is zero. Without a suitable bound, a generic declaration can appear valid and then fail for a downstream instantiation. The restricted release avoids presenting that general arithmetic problem as solved.
I prefer understanding that boundary before designing an API around it. A shared array length is directly useful. A declaration requiring arithmetic over the length asks the compiler for an additional capability. They should not be treated as the same stabilization commitment.
Owned array iteration gains an explicit API
Rust 1.51 also stabilizes std::array::IntoIter. It lets an array supply its elements by value. The beta article demonstrates arrays containing vectors, where consuming the elements is meaningfully different from iterating through references to them.
At this release, the entry point is IntoIter::new. The announcement explicitly says that adding array .into_iter behavior directly has backward-compatibility concerns because that syntax currently refers to a slice's reference iterator. A description of Rust 1.51 should therefore use the new explicit constructor for owned iteration.
The following example takes ownership of two strings through the stable constructor. The iterator supplies String values, and the loop binds each owned value before printing it.
let names = [String::from("alpha"), String::from("beta")];
for name in std::array::IntoIter::new(names) {
println!("{}", name);
}
The explicit API resolves an immediate need while preserving existing method-call behavior. It is another case where compatibility affects the route by which a capability reaches stable users. The release adds owned iteration without claiming that every familiar iteration spelling has changed meaning.
Feature unification can cross the wrong boundary
Cargo's existing resolver merges features when the same package appears through multiple dependency paths. The release announcement uses one package requesting feature A and another requesting feature B. Compiling a shared dependency with both features lets both consumers reuse one build.
That behavior has a useful economy, but it becomes troublesome when the paths have different execution contexts. A build script runs on the host, while the resulting application may target an environment without the standard library. A shared dependency can offer an optional std feature that is appropriate for the build script and inappropriate for the target application.
Under the older unification behavior, the build dependency's std feature can affect the target dependency too. The target then tries to use an unavailable standard library. The problem is not that the application deliberately selected the wrong feature. It is that feature requirements from one role were combined with another role whose environment differs.
The second resolver keeps ordinary dependency features separate from those of build dependencies and procedural macros. This preserves the distinction between host-side work and target code. The release notes describe that separation as one of the resolver's major changes.
The new resolver is an explicit choice
The release enables the new behavior through resolver version 2 in Cargo.toml. A workspace can select it in the workspace section. This fragment illustrates that selection without requiring a particular package layout.
[workspace]
resolver = "2"
Development dependencies receive another separation. Under the new resolver, their features are enabled only when the current build includes development dependencies. Target-specific dependency features likewise participate only when their target is being built. The aim is to avoid importing feature requirements from a role or platform irrelevant to the requested build.
The tradeoff is that some crates can compile more than once. Reusing one compilation with a union of all features was an optimization of the earlier behavior. Keeping feature sets apart can require separate compilations when those sets differ. The release announcement states this cost explicitly instead of presenting separation as free.
For an embedded target, that additional work can be necessary to make the intended build valid. A host-side tool and target-side library do not need identical capabilities just because they share a package name. Resolver version 2 lets Cargo preserve that distinction where its documented rules apply.
Stable subsets can have substantial effects
The const-generics subset and resolver change both make particular relationships more expressible. A library can carry supported constant values in type parameters. Cargo can distinguish feature requirements that belong to different dependency roles. Neither requires the general problem around it to disappear before delivering useful behavior.
Array authors can now state common length relationships without repeating implementations for each size. Build authors can select a resolver that respects host and target differences. The limitations remain concrete too, including arithmetic over generic constants and additional compilations for separated feature sets. Those details determine which API or build arrangement Rust 1.51 can support directly.