#23 스마트 포인터와 Box<T>

Pt J·2020년 9월 11일
1

[完] Rust Programming

목록 보기
26/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

C언어와 같은 언어를 공부해봤다면 포인터가 무엇인지 알고 있을 것이다.
포인터는 어떤 자료의 메모리 주소를 저장하는 변수다.
Rust에서는 & 기호를 통해 다른 변수를 참조하여 그 값을 대여한다.

그런데 이런 포인터의 기능을 가지고 있으면서 metadata를 포함하는 녀석들이 있다.
우리는 그 녀석들을 스마트 포인터라고 부른다.

스마트 포인터 Smart Pointer

스마트 포인터는 C++에서 유래된 개념이다.
Rust의 일반 포인터는 자료를 대여하는 것만 가능하지만
스마트 포인터는 그것을 소유할 수 있으며, 대부분 그렇게 작동한다.

스마트 포인터는 여러 가지가 있는데 우리는
표준 라이브러리가 제공하는 보편적인 스마트 포인터 몇 가지만 다루도록 하겠다.
사실 우리가 사용해온 StringVec<T>도 스마트 포인터에 포함된다.

스마트 포인터는 주로 구조체를 통해 구현되며
Deref 트레이트와 Drop 트레이트를 구현한다.
Deref 트레이트는 참조와 유관하며 Drop 트레이트는 범위를 벗어났을 때에 대한 처리를 다룬다.
이 녀석들에 대해서는 다음시간에 집중적으로 살펴보도록 하겠다.

heap 메모리를 참조하는 Box<T>

Box<T>는 자료를 heap 메모리에 저장한다는 것 외에는 별 다른 특징이 없는 녀석이다.
원래라면 stack 메모리에 저장될 자료형을 Box<T>는 heap 메모리에 저장한다.
다음과 같은 상황에 주로 사용된다.

  • 컴파일 시간에 그 크기를 알 수 없는 자료형을 사용하며 그 자료형의 값의 정확한 크기가 필요할 때
    When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size
  • 큰 자료를 사용하며 그것을 복제하지 않고 소유권을 이전하고자 할 때
    When you have a large amount of data and you want to transfer ownership but ensure the data won’t be copied when you do so
  • 특정 자료형의 값이 아니라 특정 트레이트를 구현하는 자료형의 값을 소유하고자 할 때
    When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type

예를 들어, i32 변수를 heap 메모리에 저장하기 위해서는 다음과 같이 작성할 수 있다.

peter@hp-laptop:~/rust-practice$ mkdir chapter15
peter@hp-laptop:~/rust-practice$ cd chapter15
peter@hp-laptop:~/rust-practice/chapter15$ cargo new box_i32
     Created binary (application) `box_i32` package
peter@hp-laptop:~/rust-practice/chapter15$ cd box_i32/
peter@hp-laptop:~/rust-practice/chapter15/box_i32$ vi src/main.rs

src/main.rs

fn main() {
    let b = Box::new(5);

    println!("b = {}", b);
}
peter@hp-laptop:~/rust-practice/chapter15/box_i32$ cargo run
   Compiling box_i32 v0.1.0 (/home/peter/rust-practice/chapter15/box_i32)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/box_i32`
b = 5
peter@hp-laptop:~/rust-practice/chapter15/box_i32$ 

따로 Box<T>에서 꺼내는 작업 없이 사용할 수 있다.
그리고 소유권을 가진 일반 변수가 그렇듯이 범위를 벗어나면 메모리가 해제된다.
heap 메모리의 자료와 stack 메모리의 포인터 모두 해제된다.

물론 보통은 이렇게 하나의 작은 값을 Box<T>에 저장하지는 않는다.

그렇다면 Box<T>를 사용해야만 하는 경우의 예시를 알아보자.

재귀 자료형 Recursive Type

컴파일 시간에 크기를 알 수 없는 자료형으로 재귀 자료형이 있다.
재귀 자료형은 자기 자신과 같은 자료형의 값을 참조하는 부분을 포함한 자료형이다.
이러한 자료형은 이론적으로 무한히 커질 수 있기 때문에
이를 구현하기 위해서는 Box<T>를 사용하여 구현해야 한다.

보편적인 재귀 자료형으로는 콘스 리스트ConsList; construction list가 있다.
이것은 Lisp와 그 파생 언어에 도입된, 함수형 프로그래밍에서 많이 사용되는 개념이다.
Lisp의 cons 함수는 두 개의 값으로 이루어진 쌍을 만드는데
이와 같이 어떤 값과 다음 자료로 이루어진 쌍의 리스트를 콘스 리스트라고 한다.

마지막 아이템의 두번째 값은 재귀의 base case를 의미하는 NIL을 사용한다.
// NULL과는 다르다!

Rust에서 콘스 리스트를 작성하려면
ConsNil을 열것값으로 하는 List 열거형을 사용해야 한다.
이것은 Rust 라이브러리가 제공해주지 않으며 우리가 직접 생성하여 사용한다.
Cons 열것값은 두 개의 값을 저장하는데
이를 통해 첫번째 값은 리스트에 사용할 자료형을,
두번째 값은 List 열거형을 저장하도록 하는 것이다.
그런데 List 열거형 안에 List 열거형을 그대로 넣으면
Rust 컴파일러는 그 크기가 무한해질 수 있음을 인지하고 컴파일을 거부한다.
이 때 우리가 사용할 수 있는 게 Box<T>다.

peter@hp-laptop:~/rust-practice/chapter15/box_i32$ cd ..
peter@hp-laptop:~/rust-practice/chapter15$ cargo new box_cons_list
     Created binary (application) `box_cons_list` package
peter@hp-laptop:~/rust-practice/chapter15$ cd box_cons_list/
peter@hp-laptop:~/rust-practice/chapter15/box_cons_list$ vi src/main.rs

src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil)
            ))
      ))
    );
}

Box<T>의 다른 활용은 이후에 트레이트 객체를 다루며 보게 될 것이다.

이 포스트의 내용은 공식문서의 15장 1절 Using Box<T> to Point to Data on the Heap에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글