🦀 Rust
[Rust 입문] 11. rust의 스마트 포인터
date
Mar 23, 2023
slug
rust-smart-pointer
author
status
Public
tags
Rust
독학
summary
Rust의 스마트 포인터에 대한 설명입니다.
type
Post
thumbnail
category
🦀 Rust
updatedAt
Mar 23, 2023 11:27 AM
스마트 포인터는 C++로부터 유래된 기능이다. C++에선 스마트 포인터는 단순한 포인터가 아니라 클래스이며 힙에 할당한 데이터에 대해 소유권 및 메모리 누수 방지, 참조 카운팅 등을 구현할 수 있게 한다.
rust에서 참조자와 스마트 포인터의 차이는 참조자는 단순히 데이터를 빌리기만 한다면 스마트 포인터는 데이터를 직접 소유한다.
스마트 포인터는 보통 구조체를 이용해 구현하는데 일반 구조체와 구별되는 점은
Deref 와 Drop 트레잇을 구현한다.Deref 는 구조체의 인스턴스가 참조자처럼 동작하게 하며 Drop 는 인스턴스가 스코프를 벗어날 때 동작을 설정한다.rust에선
String 및 Vec<T> 도 스마트 포인터의 일종이다.Box<T>
Box<T> 는 데이터를 힙에 저장할 수 있도록 해주는 스마트 포인터이다. 컴파일 타임에 정확한 크기를 알 수 없는 타입이나 데이터 소유권을 이동시킬 때 데이터 복사를 방지하기 위해서 주로 사용한다.예제 1
함수 스코프 내에서 내부적으로 힙에 할당하지 않는 지역변수를 할당하면 스택에 저장된다. 이런 경우 데이터 자체를 힙에 할당하기 원할 때 사용할 수 있다.
fn main() { let s = 10; // 스택에 할당됨 let h = Box::new(20); // 힙에 할당됨 println!("s : {}", s); println!("h : {}", *h); // h는 구조체의 인스턴스지만 Deref에 의해 값을 바로 뽑아낼수있다. }
예제 2 → 재귀 데이터 타입 (Recursive data type)
여러 가지 이유로 재귀적인 데이터 타입을 선언하고 사용할 경우가 있다.
자료 구조를 구현할 때에도 사용할 수 있는데 트리나 연결리스트 등을 구현할 때 타입을 재귀적으로 구현하게 된다.
lisp 프로그래밍 언어에서 유래된
cons list 라는 데이터 구조가 있는데 단일 연결 리스트와 유사하다. 이 타입의 구현의 잘못된 예이다.enum LinkedList<T> { Cons(T, LinkedList<T>), Nil, }
만약에 위처럼 구현한다면 컴파일 에러가 뜨는데 rust 컴파일러는 컴파일 타임에 LinkedList의 정확한 크기를 알고자 한다. 하지만 재귀적으로 구현되어 있으므로 rust 컴파일러는 재귀적으로 LinkedList에 접근하게 되어 E0072 (https://doc.rust-lang.org/stable/error-index.html#E0072) 에러를 발생시킨다.
이런 경우에 컴파일 타임에 정확한 크기를 알 수 없는
Box<T> 를 사용한다.fn main() { let b = LinkedList::Cons(10, Box::new(LinkedList::Nil)); if let LinkedList::Cons(t, _next) = b { println!("val : {}", t); } } enum LinkedList<T> { Cons(T, Box<LinkedList<T>>), Nil, }
Deref 트레잇
스마트 포인터의 구성 요소 중 하나인
Deref 트레잇은 역참조 연산자 * 의 동작을 정의한다.역참조 연산자
fn main() { let x = 5; let y = &x; assert_eq!(5, x); // assert_eq!(5, y); - err assert_eq!(5, *y); }
위에서 y는 참조자인데 참조자와 정수 타입은 엄연히 다르기 때문에 역참조 연산자를 이용해서 비교하여야 한다.
Deref 트레잇 구현
std::ops 에 정의되어 있는 Deref 트레잇을 이용해 구현한다.use std::ops::Deref; fn main() { let a = MyBox::new(10); assert_eq!(10, *a); } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } }
이때
*a 는 실제로 *(a.deref()) 코드를 실행한다. 그래서 deref가 참조자를 반환하고 * 연산자에 의해 참조자가 가리키는 값을 나타내게 된다.역참조 강제 (deref coercion)
함수 및 메소드의 인자에 대해 역참조 (Deref) 트레잇이 구현되어 있는 타입이 올 때 본래 타입에서 적절한 다른 타입으로 변경해 주는 것을 의미한다.
역참조 강제가 지원되지 않는다면
& 연산자나 * 연산자를 명시적으로 적어줘야 한다.위에서 만든
Mybox 를 이용하여 값을 생성하고 생성한 값을 함수나 메소드에 인자로 넣을 때fn main() { let a = MyBox::new(10); print_num(&(*a)); print_num(&a); } fn print_num(x: &i32) { println!("num : {}", x) }
&i32 값을 받는 함수에 대해 원래대로라면 &(*a) 와 같은 형태로 넣어야 한다. 왜냐 하면 *a 는 실제로 *(a.deref()) 이기 때문이다. 그래서 & 를 붙여야 참조자가 들어가게 되지만 rust는 역참조 강제라는 기능이 있어 & 연산자를 붙이면 알아서 참조자를 리턴해 준다.가변 역참조 트레잇 (DerefMut)
역참조 트레잇을 구현한 타입이 가변적이고 가리키는 값을 변경 가능하게 하려면 가변 역참조 트레잇을 구현하여야 한다.
가변 참조자를 불변 참조자로 역참조를 강제할 수는 있지만 반대의 경우에는 안된다.
Drop 트레잇
Drop 트레잇은 자원을 해제할 때와 같이 다른 용도로도 사용할 수 있고 스마트 포인터에서 자원을 해제하는 데 사용할 수 있다.
Drop 트레잇을 구현하면 인스턴스가 스코프를 벗어나면 자동으로 drop 메소드를 호출하게 해준다. 하지만 drop 메소드는 rust가 자동으로 호출하게 하므로 사용자가 임의로 메소드를 호출할 수 없고
std::mem::drop 함수를 사용해야 메소드를 호출할 수 있다.참조 카운팅 스마트 포인터 - Rc<T>
그래프 (트리 등) 자료 구조를 구현할 때 한 가지의 데이터를 다른 곳에서 여러개가 소유하게 할 때가 필요하다. 예를 들면 이진 트리에서 부모 노드는 최대 두 개의 자식 노드가 부모 노드를 가리키고 있다.
이런 경우 참조자를 이용한다면 라이프타임 때문에 모든 요소들이 전체 리스트가 살아있는 만큼 살아있게 된다. 그래서 자식 노드가 소유권을 가지고 있게 하는 것이 좋은데 이런 경우
Rc<T> 를 사용한다. 만약 아무도 특정 노드를 소유하고 있지 않다면 자동으로 할당도 해제되게 할 수 있다.이런 특징 때문에 싱글 스레드에서밖에 사용할 수 없다.
use std::rc::Rc; fn main() { let a = Rc::new(1234); let b = a.clone(); { let c = a.clone(); println!("a가 사용되고 있는 횟수 : {}", Rc::strong_count(&a)); println!("c : {}", *c); } // 스코프를 벗어나면 자동으로 카운트가 줄어든다. println!("a가 사용되고 있는 횟수 : {}", Rc::strong_count(&a)); println!("b : {}", *b); }