Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

조기 바인딩 매개변수와 후기 바인딩 매개변수

NOTE: 이 장에서는 주로 조기/후기 바인딩을 함수 항목 타입/함수 정의를 논의할 때에만 관련된 것처럼 다룹니다. 이는 완전히 옳지 않을 수 있으며, async 블록과 클로저도 이 장에서 어느 정도 다루어야 할 것입니다.

조기 바인딩 매개변수와 후기 바인딩 매개변수의 구분이 도입되었을 당시의 다음 블로그 글도 참고하십시오: Intermingled parameter lists와 Intermingled parameter lists, take 2.

“조기” 바인딩되거나 “지연” 바인딩된다는 것은 무엇을 의미합니까

모든 함수 정의에는 Fn* 트레이트를 구현하는 대응 ZST가 있으며, 이를 함수 항목 타입이라고 합니다. 이 부분에서는 함수 항목 타입의 “디슈가링“에 대해 조금 다룹니다. 이는 조기 바인딩 제네릭 매개변수와 후기 바인딩 제네릭 매개변수의 차이를 설명하는 데 유용한 맥락이 되기 때문입니다.

제네릭 매개변수가 없는 아주 단순한 예제부터 시작합시다:

#![allow(unused)]
fn main() {
fn foo(a: String) -> u8 {
    1
    /* snip */
}
}

foo에 대응하는 함수 항목 타입과 그에 연관된 Fn impl의 정의를 명시적으로 작성하면 대략 다음과 같습니다:

struct FooFnItem;

impl Fn<(String,)> for FooFnItem {
    type Output = u8;
    /* fn call(&self, ...) -> ... { ... } */
}

FnMut/FnOnce 트레이트에 대한 내장 impl과 Copy 및 Clone에 대한 impl은 간결성을 위해 생략했습니다(물론 이 트레이트들은 함수 항목 타입에 대해 실제로 구현되어 있습니다).

조금 더 복잡한 예제로, 함수에 제네릭 매개변수를 도입해 보겠습니다:

#![allow(unused)]
fn main() {
fn foo<T: Sized>(a: T) -> T {
    a
    /* snip */
}
}

정의를 작성하면 대략 다음과 같습니다:

struct FooFnItem<T: Sized>(PhantomData<fn(T) -> T>);

impl<T: Sized> Fn<(T,)> for FooFnItem<T> {
    type Output = T;
    /* fn call(&self, ...) -> ... { ... } */
}

함수 항목 타입 FooFnItem은 함수 foo에 정의된 대로 어떤 타입 매개변수 T에 대해 제네릭하다는 점에 유의하십시오. 그러나 다음 예제에서 보듯이, 함수에 정의된 제네릭 매개변수가 모두 함수 항목 타입에도 정의되는 것은 아닙니다:

#![allow(unused)]
fn main() {
fn foo<'a, T: Sized>(a: &'a T) -> &'a T {
    a
    /* snip */
}
}

이 예제를 “디슈가링“한 형태는 다음과 같습니다:

struct FooFnItem<T: Sized>(PhantomData<for<'a> fn(&'a T) -> &'a T>);

impl<'a, T: Sized> Fn<(&'a T,)> for FooFnItem<T> {
    type Output = &'a T;
    /* fn call(&self, ...) -> ... { ... } */
}

함수 foo의 라이프타임 매개변수 'a는 함수 항목 타입 FooFnItem에 존재하지 않으며, 대신 인자 타입을 표현하는 용도로만 내장 impl에 도입됩니다.

제네릭 매개변수가 모두 함수 항목 타입에 정의되지는 않으므로, 함수를 호출할 때 제네릭 인자는 두 단계에 걸쳐 제공됩니다.

  1. 함수를 이름으로 참조할 때(예: let a = foo;) FooFnItem에 대한 인자가 제공됩니다.
  2. 함수를 호출할 때(예: a(&10);) 내장 impl에 정의된 매개변수가 모두 제공됩니다.

조기/후기라는 명명 방식은 이 2단계 체계에서 유래했습니다. 조기 바인딩 매개변수는 가장 이른 단계(함수 이름 지정)에서 제공되고, 후기 바인딩 매개변수는 가장 늦은 단계(함수 호출)에서 제공됩니다.

앞의 예제의 디슈가링을 살펴보면, T는 함수 항목 타입에 나타나지만 'a는 나타나지 않으므로 T는 조기 바인딩 타입 매개변수이고 'a는 후기 바인딩 라이프타임 매개변수임을 알 수 있습니다. 각 제네릭 매개변수의 인자가 어디에서 제공되는지 주석으로 표시한, foo를 호출하는 다음 예제를 참고하십시오:

#![allow(unused)]
fn main() {
fn foo<'a, T: Sized>(a: &'a T) -> &'a T {
    a
    /* snip */
}

// Here we provide a type argument `String` to the
// type parameter `T` on the function item type
let my_func = foo::<String>;

// Here (implicitly) a lifetime argument is provided
// to the lifetime parameter `'a` on the builtin impl.
my_func(&String::new());
}

조기 바인딩 매개변수와 후기 바인딩 매개변수의 차이

고차 함수 포인터와 트레이트 바운드

제네릭 매개변수가 후기 바인딩이면 함수 항목을 더 유연하게 사용할 수 있습니다. 예를 들어, 조기 바인딩 라이프타임 매개변수를 가진 함수 foo와 후기 바인딩 라이프타임 매개변수 'a를 가진 함수 bar가 있다면, 다음과 같은 내장 Fn impl이 존재하게 됩니다:

impl<'a> Fn<(&'a String,)> for FooFnItem<'a> { /* ... */ }
impl<'a> Fn<(&'a String,)> for BarFnItem { /* ... */ }

bar 함수의 함수 항목 타입은 어떤 라이프타임의 빌림으로도 호출할 수 있으므로 bar의 시그니처가 엄밀히 더 유연합니다. 반면 foo 함수 항목 타입은 함수 항목 타입에 있는 것과 같은 라이프타임의 빌림으로만 호출할 수 있습니다. foo의 함수 항목 타입을 서로 다른 라이프타임으로 여러 번 호출해 보기만 해도 이를 확인할 수 있습니다:

#![allow(unused)]
fn main() {
// The `'a: 'a` bound forces this lifetime to be early bound.
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }

// Early bound generic parameters are instantiated here when naming
// the function `foo`. As `'a` is early bound an argument is provided.
let f = foo::<'_>;

// Both function arguments are required to have the same lifetime as
// the lifetime parameter being early bound means that `f` is only
// callable for one specific lifetime.
//
// As we call this with borrows of different lifetimes, the borrow checker
// will error here.
f(&String::new());
f(&String::new());
}

foo의 라이프타임 매개변수가 조기 바인딩되어 있으므로 f의 모든 호출자는 같은 라이프타임의 빌림을 제공해야 합니다. 이 예제에서는 foo의 함수 항목 타입을 두 번 호출하며, 매번 임시 값의 빌림을 전달합니다. 임시 값은 함수 호출 동안에만 살아 있고 그 이후에는 살아 있지 않으므로 이 두 빌림의 라이프타임은 겹칠 수 없으며, 따라서 컴파일 오류가 발생합니다.

foo의 라이프타임 매개변수가 후기 바인딩(late bound)이었다면, 각 호출자가 자신의 빌림에 대해 서로 다른 라이프타임 인자를 제공할 수 있으므로 이 코드는 컴파일될 수 있었을 것입니다. 위에서 정의한 bar 함수를 사용하여 이를 보여 주는 다음 예제를 참고하십시오:

#![allow(unused)]
fn main() {
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }

// Early bound parameters are instantiated here, however as `'a` is
// late bound it is not provided here.
let b = bar;

// Late bound parameters are instantiated separately at each call site
// allowing different lifetimes to be used by each caller.
b(&String::new());
b(&String::new());
}

이는 함수 항목 타입을 고차 함수 포인터로 강제 변환하고 고차 Fn 트레이트 바운드를 증명할 수 있는 능력에 반영됩니다. 다음 예제로 이를 보여 줄 수 있습니다:

#![allow(unused)]
fn main() {
// The `'a: 'a` bound forces this lifetime to be early bound.
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }

fn accepts_hr_fn(_: impl for<'a> Fn(&'a String) -> &'a String) {}

fn higher_ranked_trait_bound() {
    let bar_fn_item = bar;
    accepts_hr_fn(bar_fn_item);

    let foo_fn_item = foo::<'_>;
    // errors
    accepts_hr_fn(foo_fn_item);
}

fn higher_ranked_fn_ptr() {
    let bar_fn_item = bar;
    let fn_ptr: for<'a> fn(&'a String) -> &'a String = bar_fn_item;

    let foo_fn_item = foo::<'_>;
    // errors
    let fn_ptr: for<'a> fn(&'a String) -> &'a String = foo_fn_item;
}
}

두 경우 모두 빌림 검사기는 foo_fn_item을 임의의 라이프타임을 가진 빌림으로 호출할 수 있다고 보지 않으므로 오류를 발생시킵니다. 이는 foo의 라이프타임 매개변수가 조기 바인딩(early bound)되어 있어 foo_fn_item이 FooFnItem<'_> 타입을 갖게 되기 때문이며, 이 타입은 (디슈가링된 Fn impl에서 보이듯이) 동일한 라이프타임 '_를 가진 빌림으로만 호출할 수 있습니다.

후기 바인딩 매개변수가 있을 때의 터보피시

앞서 언급했듯이, 조기 바인딩 매개변수와 후기 바인딩 매개변수의 구분은 제네릭 매개변수가 인스턴스화되는 지점이 두 군데라는 것을 의미합니다:

  • 함수의 이름을 지정할 때(조기)
  • 함수를 호출할 때(후기)

현재 호출 단계에서 후기 바인딩 매개변수에 대한 제네릭 인자를 명시적으로 지정하는 구문은 없습니다. 제네릭 인자는 함수 이름을 지정할 때 조기 바인딩 매개변수에 대해서만 지정할 수 있습니다. foo::<'static>(); 구문은 함수 호출의 일부임에도 불구하고 (foo::<'static>)();처럼 동작하며, 함수 항목 타입의 조기 바인딩 제네릭 매개변수를 인스턴스화합니다.

다음 예제를 참고하십시오:

#![allow(unused)]
fn main() {
fn foo<'a>(b: &'a u32) -> &'a u32 { b }

let f /* : FooFnItem<????> */ = foo::<'static>;
}

위 예제는 라이프타임 매개변수 'a가 후기 바인딩되어 있어 “함수 이름 지정” 단계에서 인스턴스화될 수 없으므로 오류가 발생합니다. 라이프타임 매개변수를 조기 바인딩으로 만들면 이 코드가 컴파일되기 시작하는 것을 볼 수 있습니다:

#![allow(unused)]
fn main() {
fn foo<'a: 'a>(b: &'a u32) -> &'a u32 { b }

let f /* : FooFnItem<'static> */ = foo::<'static>;
}

현재 컴파일러 구현이 의도하는 바는 조기 바인딩 라이프타임 매개변수와 후기 바인딩 라이프타임 매개변수를 모두 가진 함수에 라이프타임 인자를 지정할 때 오류를 발생시키는 것입니다. 실제로는 기존 코드가 지나치게 많이 깨지는 문제 때문에, 일부 경우는 향후 호환성 경고(future compatibility warning)에 그칩니다(#42868):

  • 라이프타임 인자의 개수가 조기 바인딩 라이프타임 매개변수의 개수와 같을 때는 오류 대신 FCW가 발생합니다
  • 메서드 호출 구문을 사용할 때는 오류가 항상 FCW로 격하됩니다

이를 보여 주기 위해 여러 종류의 함수를 작성하고 각 함수에 후기 바인딩 라이프타임과 조기 바인딩 라이프타임을 하나씩 부여할 수 있습니다:

fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}

struct Foo;

trait Trait: Sized {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}

impl Trait for Foo {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

그런 다음 첫 번째 경우에 대해 각 함수를 (하나뿐인 조기 바인딩 라이프타임 매개변수에 대응하는) 라이프타임 인자 하나로 호출해 보면, 하드 오류가 아니라 FCW만 발생한다는 것을 확인할 수 있습니다.

#![allow(unused)]
#![deny(late_bound_lifetime_arguments)]

fn main() {
fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}

struct Foo;

trait Trait: Sized {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}

impl Trait for Foo {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

// Specifying as many arguments as there are early
// bound parameters is always a future compat warning
Foo.trait_method::<'static>(&(), &());
Foo::trait_method::<'static>(Foo, &(), &());
Foo::trait_function::<'static>(&(), &());
Foo.inherent_method::<'static>(&(), &());
Foo::inherent_function::<'static>(&(), &());
free_function::<'static>(&(), &());
}

두 번째 경우에 대해서는 각 함수를 (조기 바인딩이든 후기 바인딩이든) 라이프타임 매개변수 개수보다 많은 라이프타임 인자로 호출해 보면, 자유 함수와 연관 함수는 하드 오류가 발생하는 반면 메서드 호출은 FCW가 발생한다는 것을 확인할 수 있습니다:

#![allow(unused)]
fn main() {
fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}

struct Foo;

trait Trait: Sized {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}

impl Trait for Foo {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

// Specifying more arguments than there are early
// bound parameters is a future compat warning when
// using method call syntax.
Foo.trait_method::<'static, 'static, 'static>(&(), &());
Foo.inherent_method::<'static, 'static, 'static>(&(), &());
// However, it is a hard error when not using method call syntax.
Foo::trait_method::<'static, 'static, 'static>(Foo, &(), &());
Foo::trait_function::<'static, 'static, 'static>(&(), &());
Foo::inherent_function::<'static, 'static, 'static>(&(), &());
free_function::<'static, 'static, 'static>(&(), &());
}

후기 바인딩 라이프타임 매개변수와 조기 바인딩 라이프타임 매개변수 모두에 충분한 라이프타임 인자를 지정하더라도, 이 인자들은 실제로 후기 바인딩 매개변수에 제공되는 라이프타임을 표기하는 데 사용되지 않습니다. 정적이지 않은 빌림을 제공하면서 함수에 'static을 터보피시로 지정해 보면 이를 확인할 수 있습니다:

#![allow(unused)]
fn main() {
struct Foo;

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b String ) {}
}

Foo.inherent_method::<'static, 'static>(&(), &String::new());
}

&String::new() 함수 인자가 'static 라이프타임을 갖지 않는데도 이 코드는 컴파일되는데, 이는 실제로 함수를 호출할 때 “여분의” 라이프타임 인자가 후기 바인딩 매개변수에 반영되지 않고 버려지기 때문입니다.

후기 바인딩 매개변수를 가진 타입의 활성(liveness)

함수 항목 타입과 관련된 타입 outlives 바운드를 검사할 때는 조기 바인딩 매개변수를 고려합니다. 예를 들면 다음과 같습니다.

#![allow(unused)]
fn main() {
fn foo<T>(_: T) {}

fn requires_static<T: 'static>(_: T) {}

fn bar<T>() {
    let f /* : FooFnItem<T> */ = foo::<T>;
    requires_static(f);
}
}

타입 매개변수 T는 조기 바인딩이므로, foo에 대한 함수 항목 타입의 디슈가링은 struct FooFnItem<T>와 비슷한 형태가 됩니다. 그러면 FooFnItem<T>: 'static이 성립하려면 T: 'static도 성립하도록 요구해야 하며, 그렇지 않으면 건전성(soundness) 버그가 생기게 됩니다.

안타깝게도 컴파일러의 버그 때문에 조기 바인딩 라이프타임은 고려되지 않으며, 이것이 아직 해결되지 않은 건전성 버그 #84366의 원인입니다. 이는 후기 바인딩될 수 있는 유일한 종류의 제네릭 매개변수가 잘못 처리되고 있는 라이프타임뿐이므로, 활성/타입 outlives 바운드에 대해 조기 바인딩 매개변수와 후기 바인딩 매개변수 사이의 “차이“를 보여 주는 것이 불가능하다는 것을 의미합니다.

어쨌든 이론상으로는 #84366이 수정되면 아래 코드 예제가 그러한 차이를 보여 주어야 합니다:

#![allow(unused)]
fn main() {
fn early_bound<'a: 'a>(_: &'a String) {}
fn late_bound<'a>(_: &'a String) {}

fn requires_static<T: 'static>(_: T) {}

fn bar<'b>() {
    let e = early_bound::<'b>;
    // this *should* error but does not
    requires_static(e);

    let l = late_bound;
    // this correctly does not error
    requires_static(l);
}
}

매개변수가 후기 바인딩이 되기 위한 요건

라이프타임 매개변수여야 합니다

dyn for<T> Fn(Box<T>)나 for<T> fn(Box<T>) 같은 타입을 지원할 방법이 없으므로, 타입 매개변수와 상수 매개변수는 후기 바인딩이 될 수 없습니다. 이러한 타입을 호출하려면 기반 함수를 단형화(monomorphize)할 수 있어야 하는데, 동적 디스패치를 통한 간접 호출로는 이것이 불가능합니다.

where 절에서 사용되어서는 안 됩니다

현재 제네릭 매개변수가 where 절에 사용되면 그 매개변수는 반드시 조기 바인딩이어야 합니다. 예를 들면 다음과 같습니다.

#![allow(unused)]
fn main() {
trait Trait<'a> {}
fn foo<'a, T: Trait<'a>>(_: &'a String, _: T) {}
}

이 예제에서 라이프타임 매개변수 'a는 where 절 T: Trait<'a>에 나타나므로 조기 바인딩으로 간주됩니다. 이는 'a: 'a와 같은 “자명한” where 절이나 함수 인자의 적격성에 의해 암시되는 where 절에도 마찬가지로 적용됩니다. 예를 들면 다음과 같습니다:

#![allow(unused)]
fn main() {
fn foo<'a: 'a>(_: &'a String) {}
fn bar<'a, T: 'a>(_: &'a T) {}
}

이 두 함수 모두에서 라이프타임 매개변수 'a는 조기 바인딩으로 간주됩니다. 비록 그것이 사용된 where 절이 실제로는 호출자에게 아무런 제약도 부과하지 않는다고 볼 수 있더라도 그렇습니다.

이러한 제한이 존재하는 이유는 다음 두 가지가 결합된 결과입니다:

  • 후기 바인딩 매개변수에 대한 바운드는 인스턴스화되기 전까지 증명할 수 없습니다
  • 함수 포인터와 트레이트 객체에는 기반 함수에서 비롯된, 아직 증명되지 않은 where 절을 표현할 방법이 없습니다

다음 예제를 살펴보십시오:

#![allow(unused)]
fn main() {
trait Trait<'a> {}
fn foo<'a, T: Trait<'a>>(_: &'a T) {}

let f = foo::<String>;
let f = f as for<'a> fn(&'a String);
f(&String::new());
}

String은 어떤 라이프타임에 대해서도 Trait을 구현하지 않으므로, 타입 검사 중 어느 시점에는 이 코드에 대해 오류가 발생해야 합니다.

라이프타임 'a가 후기 바인딩이라면 이를 검사하기 어려워집니다. foo를 명명하는 시점에는 T: Trait<'a> 트레이트 바운드의 일부로 어떤 라이프타임을 사용해야 하는지 알 수 없습니다. 아직 인스턴스화되지 않았기 때문입니다. 함수 항목 타입을 함수 포인터로 강제 변환할 때, 함수를 호출할 때 증명해야 하는 String: Trait<'a> 트레이트 바운드를 추적할 방법이 없습니다.

라이프타임 'a가 조기 바인딩이라면(현재 rustc 구현에서는 그렇습니다), 함수 foo의 이름을 지정할 때 트레이트 바운드를 검사할 수 있습니다. where 절에 사용되는 매개변수를 조기 바인딩으로 요구하면 함수에 정의된 where 절을 검사할 자연스러운 지점이 생깁니다.

마지막으로, 라이프타임이 암시적 바운드에 사용되는 경우에는 조기 바인딩일 것을 요구하지 않습니다. 예를 들면 다음과 같습니다:

#![allow(unused)]
fn main() {
fn foo<'a, T>(_: &'a T) {}

let f = foo;
f(&String::new());
f(&String::new());
}

'a가 T: 'a의 성립을 암묵적으로 요구하는 &'a T 타입에 사용됨에도 불구하고 이 코드는 컴파일되며, 이는 해당 라이프타임 매개변수가 후기 바인딩임을 보여 줍니다. 암시적 바운드는 특별하게 취급될 수 있습니다. 암시적 바운드를 도입하는 모든 타입은 함수 포인터 타입의 시그니처에 포함되므로, 함수를 호출할 때 T: 'a를 증명해야 한다는 것을 알 수 있기 때문입니다.

인자 타입에 의해 제약되어야 합니다

함수 항목 타입에 대한 내장 impl이 제약되지 않은 제네릭 매개변수를 갖게 되지 않도록 하는 것이 중요합니다. 그렇지 않으면 비건전성으로 이어질 수 있기 때문입니다. 이는 사용자가 작성한 impl에 적용되는 것과 같은 종류의 제한으로, 예를 들어 다음 코드는 오류를 일으킵니다:

#![allow(unused)]
fn main() {
trait Trait {
    type Assoc;
}

impl<'a> Trait for u8 {
    type Assoc = &'a String;
}
}

함수 항목에 대한 내장 impl의 유사한 예는 다음과 같습니다:

fn foo<'a>() -> &'a String { /* ... */ }

라이프타임 매개변수 'a가 후기 바인딩이라면 제약되지 않은 라이프타임을 가진 내장 impl이 생기게 됩니다. 이를 보여 주기 위해 'a가 후기 바인딩인 경우의 함수 항목 타입과 그 impl에 대한 디슈가링을 직접 작성해 볼 수 있습니다:

// NOTE: this is just for demonstration, in practice `'a` is early bound
struct FooFnItem;

impl<'a> Fn<()> for FooFnItem {
    type Output = &'a String;
    /* fn call(...) -> ... { ... } */
}

이러한 상황을 피하기 위해 'a를 조기 바인딩으로 간주합니다. 그러면 impl의 라이프타임이 self 타입에 의해 제약됩니다:

struct FooFnItem<'a>(PhantomData<fn() -> &'a String>);

impl<'a> Fn<()> for FooFnItem<'a> {
    type Output = &'a String;
    /* fn call(...) -> ... { ... } */
}