hyunsu blog

홈강좌Rust 입문·중급 47편 중 38편째

Rust Box — 값을 힙에 두고 크기가 정해진 포인터로 다룬다 (E0072)

목차 · 4
  1. 01스택과 힙
  2. 02Box::new 로 힙에 둔다
  3. 03자기 자신을 칸으로 가지면 E0072
  4. 04실습 — Box 로 감싼 연결 목록

Rust 의 Box<T> 는 값을 힙에 두고, 그 자리를 가리키는 크기가 정해진 포인터다. 자기 자신을 칸으로 갖는 재귀 타입은 크기가 끝없이 커져서 E0072 로 막히는데, 그 칸을 Box 로 감싸면 크기가 정해져 컴파일된다.

01 스택과 힙

프로그램이 값을 두는 곳은 크게 둘이다.

스택힙
두는 것크기가 컴파일할 때 정해진 값크기가 실행 중에 정해지거나 큰 값
예i32 · bool · 고정 길이 배열String · Vec 의 내용
빠르기아주 빠르다자리를 찾는 만큼 느리다

15편부터 본 String 은 사실 글자를 힙에 두고, 변수에는 그 자리를 가리키는 작은 정보만 둔다. 주인이 블록을 나갈 때 힙의 글자를 치우는 것이 소유권 규칙이었다.

02 Box::new 로 힙에 둔다

Box 는 그 일을 아무 값에나 해 준다.

rust
fn main() {
    let n = Box::new(5);
    let doubled = *n * 2;
    println!("{n} {doubled}");
}
text
   Compiling boxes v0.1.0 (~/projects/boxes)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/boxes`
5 10

Box::new(5) 는 5 를 힙에 두고, n 은 그 자리를 가리킨다. 찍을 때는 그냥 {n} 으로 되고, 계산할 때는 *n 으로 가리키는 값을 꺼낸다(28편의 *s 와 같다). n 이 블록을 나가면 힙의 5 도 치워진다. 정수 하나를 힙에 둘 일은 거의 없다. Box 가 꼭 필요한 곳은 아래 같은 경우다.

03 자기 자신을 칸으로 가지면 E0072

연결 목록을 열거형으로 만들어 보자. 한 칸은 숫자 하나와 "나머지 목록" 을 갖거나, 끝(End)이다.

rust
#[derive(Debug)]
enum List {
    Node(i32, List),
    End,
}

fn main() {
    let list = List::Node(1, List::Node(2, List::End));
    println!("{list:?}");
}
text
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:2:1
  |
2 | enum List {
  | ^^^^^^^^^
3 |     Node(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
3 |     Node(i32, Box<List>),
  |               ++++    +
  1. 무엇 — 재귀 타입 List 의 크기가 무한하다(infinite size).
  2. 어디 — 3번째 줄의 List. 간접 참조 없이 자기 자신을 품었다(recursive without indirection).
  3. 고치는 법 — help: 가 Box 나 Rc, & 를 끼우라고 하고, Box<List> 로 고친 줄을 보여 준다.

Rust 는 컴파일할 때 타입의 크기를 알아야 한다. List 의 크기는 i32 + List 의 크기인데, 그 안의 List 는 또 i32 + List 다. 끝이 없다. 실제로는 뒤에 이어지는 오류(E0391)도 같은 원인이라 함께 고쳐진다.

04 실습 — Box 로 감싼 연결 목록

Box<List> 는 "힙 어딘가에 있는 List 를 가리키는 포인터" 다. 포인터의 크기는 늘 같으니 List 의 크기가 정해진다.

rust
#[derive(Debug)]
enum List {
    Node(i32, Box<List>),
    End,
}

fn sum(list: &List) -> i32 {
    match list {
        List::Node(value, rest) => value + sum(rest),
        List::End => 0,
    }
}

fn main() {
    let list = List::Node(1, Box::new(List::Node(2, Box::new(List::Node(3, Box::new(List::End))))));
    println!("{list:?}");
    println!("sum = {}", sum(&list));
}
text
   Compiling list v0.1.0 (~/projects/list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/list`
Node(1, Node(2, Node(3, End)))
sum = 6
  • List::Node(1, Box::new(…)) 처럼 다음 칸을 Box::new 로 감싸 만든다.
  • sum 은 자기 자신을 부르는 재귀 함수다. Node 면 값에 나머지의 합을 더하고, End 면 0 이다(25편의 match).
  • rest 는 &Box<List> 인데, sum(rest) 에 그대로 넘겨도 &List 로 맞춰진다. Box 가 가리키는 값처럼 행동하기 때문이다.

목록 끝에 4 를 하나 더 이어 보자. 괄호가 하나씩 늘어난다. 이런 모양은 트리·그래프 같은 자료 구조에 그대로 쓰인다. 다만 Box 는 주인이 하나뿐이라, 한 칸을 둘이 함께 가리킬 수는 없다. 그것은 다음 편의 Rc 가 한다.