수명 생략

수명 생략

참조를 다룰 때마다 수명 파라미터('a 같은 것)를 일일이 적어 주는 건 정말 번거롭죠. Rust는 그래서 컴파일러가 합리적인 기본값을 유추할 수 있는 여러 위치에서 수명을 생략(omit) 할 수 있게 해 줘요. 이 규칙을 수명 생략(lifetime elision) 이라고 불러요.

출처: Rust Reference

본문

함수에서의 수명 생략 (Lifetime elision in functions)

흔한 패턴을 더 쓰기 편하게 만들기 위해, 함수 아이템(function item), 함수 포인터(function pointer), 클로저 트레이트 시그니처(closure trait signature) 에서 수명 파라미터를 생략할 수 있어요. 생략된 수명의 수명 파라미터를 유추할 때는 다음 규칙을 사용해요.

  • 유추할 수 없는 수명 파라미터를 생략하는 것은 오류예요.
  • 자리 표시자 수명(placeholder lifetime)'_도 같은 방식으로 수명을 유추하게 할 수 있어요. 경로 안의 수명에서는 '_를 쓰는 쪽이 선호돼요.
  • 트레이트 객체 수명은 아래에서 다루는 별도의 규칙을 따라요.

구체적인 유추 규칙은 이렇게 돼요.

  • 파라미터에 생략된 수명 하나하나는 각각 별개의 수명 파라미터가 돼요.
  • 파라미터에 수명이 정확히 하나 쓰였다면(생략됐든 아니든), 그 수명이 생략된 모든 출력 수명에 할당돼요.
  • 메서드 시그니처에는 규칙이 하나 더 있어요. 수신자(receiver) 의 타입이 &Self 또는 &mut Self라면, 그 Self에 대한 참조의 수명이 생략된 모든 출력 수명 파라미터에 할당돼요.

예시를 볼게요. 생략된(ellided) 형태와 펼쳐진(expanded) 형태가 나란히 보이죠.

#![allow(unused)]
fn main() {
trait T {}
trait ToCStr {}
struct Thing<'a> {f: &'a i32}
struct Command;

trait Example {
fn print1(s: &str);                                   // elided
fn print2(s: &'_ str);                                // also elided
fn print3<'a>(s: &'a str);                            // expanded

fn debug1(lvl: usize, s: &str);                       // elided
fn debug2<'a>(lvl: usize, s: &'a str);                // expanded

fn substr1(s: &str, until: usize) -> &str;            // elided
fn substr2<'a>(s: &'a str, until: usize) -> &'a str;  // expanded

fn get_mut1(&mut self) -> &mut dyn T;                 // elided
fn get_mut2<'a>(&'a mut self) -> &'a mut dyn T;       // expanded

fn args1<T: ToCStr>(&mut self, args: &[T]) -> &mut Command;                  // elided
fn args2<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command; // expanded

fn other_args1<'a>(arg: &str) -> &'a str;             // elided
fn other_args2<'a, 'b>(arg: &'b str) -> &'a str;      // expanded

fn new1(buf: &mut [u8]) -> Thing<'_>;                 // elided - preferred
fn new2(buf: &mut [u8]) -> Thing;                     // elided
fn new3<'a>(buf: &'a mut [u8]) -> Thing<'a>;          // expanded
}

type FunPtr1 = fn(&str) -> &str;                      // elided
type FunPtr2 = for<'a> fn(&'a str) -> &'a str;        // expanded

type FunTrait1 = dyn Fn(&str) -> &str;                // elided
type FunTrait2 = dyn for<'a> Fn(&'a str) -> &'a str;  // expanded
}

반대로, 수명 파라미터를 생략할 수 없는 상황도 있어요. 다음 예시를 보면 그 이유가 분명해져요.

#![allow(unused)]
fn main() {
// The following examples show situations where it is not allowed to elide the
// lifetime parameter.

trait Example {
// Cannot infer, because there are no parameters to infer from.
fn get_str() -> &str;                                 // ILLEGAL

// Cannot infer, ambiguous if it is borrowed from the first or second parameter.
fn frob(s: &str, t: &str) -> &str;                    // ILLEGAL
}
}

get_str은 유추할 파라미터가 하나도 없고, frob은 첫 번째 파라미터에서 빌려온 건지 두 번째에서 빌려온 건지 애매하기 때문에 둘 다 오류예요.

기본 트레이트 객체 수명 (Default trait object lifetimes)

트레이트 객체가 보유한 참조의 가정된 수명을 기본 객체 수명 제약(default object lifetime bound) 이라고 불러요. 이 제약은 RFC 599에서 정의되고 RFC 1156에서 수정되었어요.

수명 제약(lifetime bound)을 아예 생략하면, 위에서 본 수명 파라미터 생략 규칙 대신 이 기본 객체 수명 제약이 사용돼요. 만약 '_를 수명 제약으로 쓴다면 그 제약은 평소의 생략 규칙을 따라요.

트레이트 객체가 제네릭 타입의 타입 인자로 쓰이면, 그 포함 타입(containing type) 을 먼저 사용해 제약을 유추하려 해요.

  • 포함 타입에서 유일한 제약이 나온다면 그것이 기본값이 돼요.
  • 포함 타입에서 제약이 둘 이상 나온다면 반드시 명시적 제약을 지정해야 해요.

둘 다 해당하지 않으면, 트레이트에 있는 제약을 사용해요.

  • 트레이트가 단일 수명 제약으로 정의되었다면 그 제약을 사용해요.
  • 어떤 수명 제약에든 'static이 쓰였다면 'static을 사용해요.
  • 트레이트에 수명 제약이 없다면, 표현식 안에서는 수명이 유추되고, 표현식 밖에서는 'static 이 돼요.
#![allow(unused)]
fn main() {
// For the following trait...
trait Foo { }

// These two are the same because Box<T> has no lifetime bound on T
type T1 = Box<dyn Foo>;
type T2 = Box<dyn Foo + 'static>;

// ...and so are these:
impl dyn Foo {}
impl dyn Foo + 'static {}

// ...so are these, because &'a T requires T: 'a
type T3<'a> = &'a dyn Foo;
type T4<'a> = &'a (dyn Foo + 'a);

// std::cell::Ref<'a, T> also requires T: 'a, so these are the same
type T5<'a> = std::cell::Ref<'a, dyn Foo>;
type T6<'a> = std::cell::Ref<'a, dyn Foo + 'a>;
}

반대로, 포함 타입에서 제약을 유추할 수 없는 경우 오류가 나요. 다음 예시가 그 경우예요.

#![allow(unused)]
fn main() {
// This is an example of an error.
trait Foo { }
struct TwoBounds<'a, 'b, T: ?Sized + 'a + 'b> {
    f1: &'a i32,
    f2: &'b i32,
    f3: T,
}
type T7<'a, 'b> = TwoBounds<'a, 'b, dyn Foo>;
//                                  ^^^^^^^
// Error: the lifetime bound for this object type cannot be deduced from context
}

TwoBounds'a'b 두 제약을 요구하는데 어느 걸 쓸지 결정할 수 없어서 오류가 나는 거예요.

한 가지 유의할 점이 있어요. 가장 안쪽의 객체가 제약을 결정해요. 그래서 &'a Box<dyn Foo>는 여전히 &'a Box<dyn Foo + 'static>이에요.

#![allow(unused)]
fn main() {
// For the following trait...
trait Bar<'a>: 'a { }

// ...these two are the same:
type T1<'a> = Box<dyn Bar<'a>>;
type T2<'a> = Box<dyn Bar<'a> + 'a>;

// ...and so are these:
impl<'a> dyn Bar<'a> {}
impl<'a> dyn Bar<'a> + 'a {}
}

const와 static에서의 생략 (const and static elision)

참조 타입을 가진 상수(constant)와 static 선언 모두 명시적 수명이 없다면 암묵적으로 'static 수명을 가져요. 그래서 위에서 다룬 'static이 포함된 상수 선언은 수명을 생략하고 써도 돼요.

#![allow(unused)]
fn main() {
// STRING: &'static str
const STRING: &str = "bitstring";

struct BitsNStrings<'a> {
    mybits: [u32; 2],
    mystring: &'a str,
}

// BITS_N_STRINGS: BitsNStrings<'static>
const BITS_N_STRINGS: BitsNStrings<'_> = BitsNStrings {
    mybits: [1, 2],
    mystring: STRING,
};
}

한 가지 더 짚어둘 게 있어요. static이나 const 아이템에 함수·클로저 참조가 포함되어 있고, 그 참조들이 또 참조를 포함한다면, 컴파일러는 먼저 표준 생략 규칙을 시도해요. 평소 규칙으로 수명을 해결하지 못하면 오류가 나요.

#![allow(unused)]
fn main() {
struct Foo;
struct Bar;
struct Baz;
fn somefunc(a: &Foo, b: &Bar, c: &Baz) -> usize {42}
// Resolved as `for<'a> fn(&'a str) -> &'a str`.
const RESOLVED_SINGLE: fn(&str) -> &str = |x| x;

// Resolved as `for<'a, 'b, 'c> Fn(&'a Foo, &'b Bar, &'c Baz) -> usize`.
const RESOLVED_MULTIPLE: &dyn Fn(&Foo, &Bar, &Baz) -> usize = &somefunc;
}

반면 다음 예시처럼 반환 참조의 수명을 인자 수명과 연결할 충분한 정보가 없으면 오류예요.

#![allow(unused)]
fn main() {
struct Foo;
struct Bar;
struct Baz;
fn somefunc<'a,'b>(a: &'a Foo, b: &'b Bar) -> &'a Baz {unimplemented!()}
// There is insufficient information to bound the return reference lifetime
// relative to the argument lifetimes, so this is an error.
const RESOLVED_STATIC: &dyn Fn(&Foo, &Bar) -> &Baz = &somefunc;
//                                            ^
// this function's return type contains a borrowed value, but the signature
// does not say whether it is borrowed from argument 1 or argument 2
}

더 알아보기 (Learn more)