제네릭 매개변수
제네릭 매개변수 (Generic Parameters)
타입·상수·수명을 매개변수로 받아 재사용되는 코드를 만들고 싶을 때 제네릭이 필요해요. Rust의 제네릭은 함수·구조체·트레잇 등 다양한 아이템을 대상으로 하며, 특히 상수 제네릭(const generics)이 꽤 세밀한 규칙을 갖고 있습니다. 이 문서에서 그 문법과 제약을 하나씩 정리해 볼게요.
출처: Rust Reference
문법 (Syntax)
GenericParams → < ( GenericParam ( , GenericParam )* ,? )? >
GenericParam → OuterAttribute* ( LifetimeParam | TypeParam | ConstParam )
LifetimeParam → Lifetime ( : LifetimeBounds? )?
TypeParam → IDENTIFIER ( : Bounds? )? ( = Type )?
ConstParam → const IDENTIFIER : Type
( = ( BlockExpression | IDENTIFIER | -? LiteralExpression ) )?
함수, 타입 별칭, struct, 열거형, union, 트레잇, 구현은 타입·상수·수명으로 매개변수화(parameterized) 될 수 있습니다. 이런 매개변수들은 각괄호(<...>) 안에 나열되며, 보통 아이템 이름 바로 뒤, 그 정의 앞에 옵니다. 이름이 없는 구현(impl)의 경우에는 impl 바로 뒤에 옵니다.
제네릭 매개변수의 순서는 제한되어 있습니다. 수명 매개변수가 먼저 오고, 그다음에 타입·상수 매개변수가 섞여 옵니다.
GenericParams 목록 안에서 같은 매개변수 이름을 두 번 이상 선언할 수 없습니다.
타입·상수·수명 매개변수를 가진 아이템의 예를 볼게요.
#![allow(unused)]
fn main() {
fn foo<'a, T>() {}
trait A<U> {}
struct Ref<'a, T> where T: 'a { r: &'a T }
struct InnerArray<T, const N: usize>([T; N]);
struct EitherOrderWorks<const N: bool, U>(U);
}
제네릭 매개변수는 선언된 아이템 정의 안에서 스코프에 포함됩니다. 함수 본문 안에서 선언된 아이템에는 스코프가 미치지 않는데, 이는 아이템 선언에서 설명한 대로예요. 자세한 내용은 제네릭 매개변수 스코프를 참고하세요.
참조, 원시 포인터, 배열, 슬라이스, 튜플, 함수 포인터도 수명·타입 매개변수를 갖지만, 경로(path) 문법으로 다뤄지지는 않습니다.
'_와 'static은 수명 매개변수 이름으로 쓸 수 없습니다.
상수 제네릭 (Const Generics)
상수 제네릭 매개변수(const generic parameter) 는 아이템이 상수 값에 대해 제네릭이 되도록 해줍니다.
const 식별자는 값 네임스페이스에 상수 매개변수의 이름을 도입하며, 아이템의 모든 인스턴스는 주어진 타입의 값으로 인스턴스화되어야 합니다.
const 매개변수에 허용되는 타입은 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, char, bool뿐입니다.
const 매개변수는 const 아이템이 쓰일 수 있는 어디든 쓸 수 있지만, 타입이나 배열 반복 표현식 안에서 쓸 때는 반드시 단독(standalone)이어야 한다는 예외가 있습니다(아래 설명). 즉 다음 위치에서 허용됩니다:
- 해당 아이템의 시그니처 일부를 이루는 어떤 타입에 적용된 const로.
- 연관 const(associated const)를 정의하는 const 표현식의 일부로, 또는 연관 타입(associated type)의 매개변수로.
- 아이템 안 어떤 함수 본문의 런타임 표현식에서 값으로.
- 아이템 안 어떤 함수 본문에 쓰인 어떤 타입의 매개변수로.
- 아이템 안 어떤 필드의 타입의 일부로.
#![allow(unused)]
fn main() {
// Examples where const generic parameters can be used.
// Used in the signature of the item itself.
fn foo<const N: usize>(arr: [i32; N]) {
// Used as a type within a function body.
let x: [i32; N];
// Used as an expression.
println!("{}", N * 2);
}
// Used as a field of a struct.
struct Foo<const N: usize>([i32; N]);
impl<const N: usize> Foo<N> {
// Used as an associated constant.
const CONST: usize = N * 4;
}
trait Trait {
type Output;
}
impl<const N: usize> Trait for Foo<N> {
// Used as an associated type.
type Output = [i32; N];
}
}
반대로, const 매개변수를 쓸 수 없는 위치도 있어요.
#![allow(unused)]
fn main() {
// Examples where const generic parameters cannot be used.
fn foo<const N: usize>() {
// Cannot use in item definitions within a function body.
const BAD_CONST: [usize; N] = [1; N];
static BAD_STATIC: [usize; N] = [1; N];
fn inner(bad_arg: [usize; N]) {
let bad_value = N * 2;
}
type BadAlias = [usize; N];
struct BadStruct([usize; N]);
}
}
추가 제약으로, const 매개변수는 타입이나 배열 반복 표현식 안에서 단독 인수로만 등장할 수 있습니다. 그런 문맥에서는 단일 세그먼트 경로 표현식으로만 쓸 수 있으며, 블록 안에 둘 수는 있습니다(N이나 {N}처럼요). 즉 다른 표현식과 결합할 수 없습니다.
#![allow(unused)]
fn main() {
// Examples where const parameters may not be used.
// Not allowed to combine in other expressions in types, such as the
// arithmetic expression in the return type here.
fn bad_function<const N: usize>() -> [u8; {N + 1}] {
// Similarly not allowed for array repeat expressions.
[1; {N + 1}]
}
}
경로 안의 const 인수는 해당 아이템에 쓸 const 값을 지정합니다.
그 인수는 추론된 const이거나, const 매개변수에 주어진 타입의 const 표현식이어야 합니다. const 표현식은 단일 경로 세그먼트(IDENTIFIER)나 (선행 - 토큰을 가질 수 있는) 리터럴이 아니라면 반드시 블록 표현식(중괄호로 감싼)이어야 합니다.
참고 이 구문 제약은 타입 안의 표현식을 파싱할 때 무한한 lookahead가 필요하지 않도록 하기 위한 것입니다.
#![allow(unused)]
fn main() {
struct S<const N: i64>;
const C: i64 = 1;
fn f<const N: i64>() -> S<N> { S }
let _ = f::<1>(); // Literal.
let _ = f::<-1>(); // Negative literal.
let _ = f::<{ 1 + 2 }>(); // Constant expression.
let _ = f::<C>(); // Single segment path.
let _ = f::<{ C + 1 }>(); // Constant expression.
let _: S<1> = f::<_>(); // Inferred const.
let _: S<1> = f::<(((_)))>(); // Inferred const.
}
참고 제네릭 인수 목록에서 추론된 const는 추론된 타입으로 파싱되지만, 의미론적으로는 별도의 const 제네릭 인수로 취급됩니다.
const 인수가 기대되는 위치에서, _(임의 개수의 맞는 괄호로 감싸도 됨)를 대신 쓸 수 있는데 이를 추론된 const(inferred const) 라고 부릅니다. 이는 주변 정보를 바탕으로 가능하면 const 인수를 추론하도록 컴파일러에 요청하는 것입니다.
#![allow(unused)]
fn main() {
fn make_buf<const N: usize>() -> [u8; N] {
[0; _]
// ^ Infers `N`.
}
let _: [u8; 1024] = make_buf::<_>();
// ^ Infers `1024`.
}
참고 추론된 const는 의미론적으로 표현식이 아니므로 중괄호 안에서는 받아들여지지 않습니다.
#![allow(unused)] fn main() { fn f<const N: usize>() -> [u8; N] { [0; _] } let _: [_; 1] = f::<{ _ }>(); // ^ ERROR `_` not allowed here }
추론된 const는 아이템 시그니처 안에서는 쓸 수 없습니다.
#![allow(unused)]
fn main() {
fn f<const N: usize>(x: [u8; N]) -> [u8; _] { x }
// ^ ERROR not allowed
}
제네릭 인수가 타입 인수인지 const 인수인지 모호할 때는 항상 타입으로 해석됩니다. 인수를 블록 표현식에 넣으면 const 인수로 해석되도록 강제할 수 있습니다.
#![allow(unused)]
fn main() {
type N = u32;
struct Foo<const N: usize>;
// The following is an error, because `N` is interpreted as the type alias `N`.
fn foo<const N: usize>() -> Foo<N> { todo!() } // ERROR
// Can be fixed by wrapping in braces to force it to be interpreted as the `N`
// const parameter:
fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ok
}
타입·수명 매개변수와 달리, const 매개변수는 제네릭 구현에서 설명하는 구현의 경우를 제외하고, 매개변수화된 아이템 안에서 사용되지 않고도 선언될 수 있습니다.
#![allow(unused)]
fn main() {
// ok
struct Foo<const N: usize>;
enum Bar<const M: usize> { A, B }
// ERROR: unused parameter
struct Baz<T>;
struct Biz<'a>;
struct Unconstrained;
impl<const N: usize> Unconstrained {}
}
트레잇 바운드 의무(trait bound obligation)를 해석할 때, 바운드가 충족되는지 판단하는 데 const 매개변수의 모든 구현의 완전함(exhaustiveness)은 고려되지 않습니다. 예를 들어 아래에서 bool 타입의 모든 가능한 const 값이 구현되어 있음에도, 여전히 트레잇 바운드가 충족되지 않는다는 에러가 납니다.
#![allow(unused)]
fn main() {
struct Foo<const B: bool>;
trait Bar {}
impl Bar for Foo<true> {}
impl Bar for Foo<false> {}
fn needs_bar(_: impl Bar) {}
fn generic<const B: bool>() {
let v = Foo::<B>;
needs_bar(v); // ERROR: trait bound `Foo<B>: Bar` is not satisfied
}
}
where 절 (Where Clauses)
문법 (Syntax)
WhereClause → where ( WhereClauseItem , )* WhereClauseItem?
WhereClauseItem →
LifetimeWhereClauseItem
| TypeBoundWhereClauseItem
LifetimeWhereClauseItem → Lifetime : LifetimeBounds?
TypeBoundWhereClauseItem → ForLifetimes? Type : Bounds?
where 절 은 타입·수명 매개변수에 대한 바운드를 지정하는 또 다른 방법이며, 동시에 타입 매개변수가 아닌 타입에 대한 바운드도 지정할 수 있게 해줍니다.
for 키워드는 higher-ranked 수명을 도입하는 데 쓰입니다. 오직 LifetimeParam 매개변수만 허용해요.
#![allow(unused)]
fn main() {
struct A<T>
where
T: Iterator, // Could use A<T: Iterator> instead
T::Item: Copy, // Bound on an associated type
String: PartialEq<T>, // Bound on `String`, using the type parameter
i32: Default, // Allowed, but not useful
{
f: T,
}
}
속성 (Attributes)
제네릭 수명·타입 매개변수에는 속성을 붙일 수 있습니다. 이 위치에서 아무것도 하는 내장 속성은 없지만, 커스텀 derive 속성이 의미를 부여할 수는 있어요.
아래 예시는 커스텀 derive 속성을 이용해 제네릭 매개변수의 의미를 바꾸는 모습을 보여줍니다.
// Assume that the derive for MyFlexibleClone declared `my_flexible_clone` as
// an attribute it understands.
#[derive(MyFlexibleClone)]
struct Foo<#[my_flexible_clone(unbounded)] H> {
a: *const H
}
더 알아보기 (Learn more)
- 트레잇 바운드 (Trait Bounds) — 바운드·higher-ranked 수명
- 상수 아이템 (Constant Items) — const 아이템 사용처
- 구현 (Implementations) — 제네릭 구현 규칙
- 네임스페이스 (Namespaces) — 값·타입 네임스페이스