티스토리 뷰

Rust-Language

스마트 포인터

kmj24 2021. 5. 16. 17:38

포인터는 메모리의 주소값을 담고있는 변수이다.

주소값은 다른 데이터를 가리킨다. (참조)

Rust의 가장 흔한 종류의 포인터는 참조자인데 & 키워드를 이용하고 이들이 가리키고 있는 값을 빌린다.

 

스마트 포인터

 포인터 처럼 작동하지만 추가적인 기능이 있는 데이터 구조이다.

 C++에서 유래 되었으며, 메모리 누수(memory leak)로 부터 프로그램의 안전성을 보장하기 위해 스마트 포인터를 제공한다. 포인터 처럼 동작하며 사용이 끝난 메모리를 자동으로 해제해준다.

 C++에서는 new 키워드를 사용하여 기본 보인터가 실제 메모리를 가리키도록 초기화 한 후, 기본 포인터를 스마트 포인터에 대입하여 사용한다. 정의된 스마트 포인터의 수명이 다하면 소멸자는 delete 키워드를 사용하여 자동으로 메모리를 해제한다.(new 키워드가 반환하는 주소값을 스마트 포인터에 대입하면 따로 메모리를 해제할 필요가 없어짐)

소유권과 빌림의 개념을 가지고 있는 Rust에서는, 참조자와 스마트 포인터 간 추가적인 차이점은

 - 참조자가 데이터를 빌리기만 하는 포인터

 - 스마트 포인터는 가리키고 있는 데이터를 소유한다.

String과 Vec<T>도 스마트 포인터의 일종이다.

보통 구조체를 이용하여 구현되어 있다. 

Deref, Drop 트레잇을 구현한다.

 - Deref 트레잇 : 스마트 포인터 구조체의 인스턴스가 참조자처럼 동작하도록 하여 참조자나 스마트 포인터 둘 중 하나와 함께 작동하는 코드를 작성하도록 해줌

 - Drop 트레잇 : 스마트 포인터의 인스턴스가 Scope 밖으로 벗어났을 때 실행 되는 코드이다.

 

표준 라이브러리 내 가장 많이 쓰이는 스마트 포인터

 - Box<T> : 값을 heap에 할당

 - Rc<T> : 복수개의 소유권을 가능하게 하는 첨조 카운팅 타입

 - RefCell<T> : 빌림 규칙을 컴파일 타임이 아닌 런타임에 강제하는 타입

 - Ref<T>, RefMut<T> : RefCell<T>를 통해 접근 가능

 

Box<T>

heap에 있는 데이터를 가리킨다. 

데이터를 Stack이 아닌 heap에 저장할 수 있도록 해준다.

스택 대신 heap에 데이터를 저장한다는 점 외의 성능적 Overhead는 없다.

Box<T>를 쓰게 될 상황

 - 컴파일 타임에 크기를 알 수 없는 타입을 갖고 있고, 정확한 사이즈를 알 필요가 있는 맥락 안에서 해당 타입의 값을 이용하고 싶을 때

 - 커다란 데이터를 가지고 있고, 소유권을 옮기고 싶지만, 그렇게 했을 때 데이터가 복사되지 않을 것이라고 보장하기를 원할 경우

 - 어떤 값을 소유하고 이 값의 구체화된 타입을 알고 있기보다는 특정 트레잇을 구현한 타입이라는 점만 신경쓰고 싶을 때 

 

Box<T>를 사용하여 heap에 데이터를 저장하기

fn _box(){
    let b = Box::new(5);    
    println!("{}", b);
}

5를 heap에 할당했다.

stack에 있는 데이터와 유사한 방식으로 Box 내의 데이터에 접근할 수 있다.

 

재귀적 타입

cons list 데이터 구조이다.

 cons 함수 개념은 to cons x onto y라는 약식으로 요소 x를 새로운 컨테이너에 집어 넣고, 그 다음 컨테이너 y를 넣는 식으로 새로운 컨테이너 인스턴스를 생성하는 것을 의미한다.

 cons list내의 각 아이템은 현재 아이템의 값과 다음 아이템을 가지고 있으며, 리스트의 마지막 아이템은 다음 아이템 없이 Nil이라는 값을 담는다.

cons list 는 cons 함수를 재귀적으로 호출함으로써 만들어지고 Nil은 재귀의 default case이다.

enum List {
    Cons(i32, List),
    Nil
}

use List::{Cons, Nil};

fn cons_list() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

pub fn run(){
    _box();
}

위의 코드를 실행했을 경우 다음의 오류를 얻을 수 있다.

이 오류는 타입이 무한한 크기를 갖는다 라고 한다.

원인은 재귀적인 variant를 이용하여 List를 정의했기 때문이다.

출처 : https://rinthel.github.io/rust-lang-book-ko/ch15-01-box.html

Box<T>를 사용하여 해결해보자.

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

use List::{Cons, Nil};

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

pub fn run(){
    _box();
    cons_list();
}

List는 Cons가 Box를 가지고 있으므로 무한한 크기가 아니게 된다.

Box<T>가 포인터이므로 Rust는 언제나 Box<T>가 필요로 하는 공간이 얼마인지 알고 있다.

포인터의 크기는 가리키고 있는 데이터의 양에 기반하여 변경되지 않는다. (주소를 가리키고 있으므로)

 

참조 : https://rinthel.github.io/rust-lang-book-ko/ch15-01-box.html

 

Box는 힙에 있는 데이터를 가리키고 알려진 크기를 갖습니다 - The Rust Programming Language

가장 직관적인 스마트 포인터는 박스 (box) 인데, 이 타입은 Box 라고 쓰입니다. 박스는 여러분이 데이터를 스택이 아니라 힙에 저장할 수 있도록 해줍니다. 스택에 남는 것은 힙 데이터를 가리키

rinthel.github.io

Deref

Deref 트레잇을 구현하는것은 역참조 연산자 * 의 동작을 커스터마이징 하는것을 허용한다.

스마트 포인터가 평범한 참조자 처럼 취급 될 수 있는 방식으로 Deref를 구현함으로써, 참조자에 대해 작동하는 코드를 작성하고 이 코드를 또한 스마트 포인터에서도 사용할 수 있다.

fn deref_oper(){
    let x = 5;
    let y = &x;
    
    assert_eq!(5, *y);
    assert_eq!(5, y);
}

assert_eq!(5, y);에서 컴파일 오류가 발생한다.

숫자와 숫자에 대한 참조자를 비교하는 것을 허용하지 않으며 *를 사용하여 해당 참조자를 따라가 가리키고 있는 값을 얻어야 한다.

Box<T>를 참조자처럼 사용하기

fn deref_oper(){
    let x = 5;
    let y = Box::new(x);
    
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

위의 코드처럼 사용할 수 있다.

다른 점은 오직 x의 값을 가리키는 참조자가 아닌 x를 가리키는 박스의 인스턴스로 y를 설정한 것이다.

 

Deref 트레잇을 구현하여 임의의 타입을 참조자처럼 다루기

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn deref_oper(){
    let x = 5;
    let y = MyBox::new(x);
    
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

type Target = T; 문법은 Deref 트레잇이 사용할 연관 타입을 정의한다.

deref 메소드의 반환값은 &self.0이므로 deref는 *연산자로 역참조가 가능하여 접근하고자 하는 참조자를 반환한다.

Deref트레잇이 없다면 &참조자들만 역참조가 가능하다. 

Deref를 구현한 어떠한 타입의 값을 가지고 &참조자를 가져오기 위해 어떻게 역참조하는지 알고있는 deref 메소드를 호출하는 기능을 부여한다.

따라서 *y는 실제로 *(y.deref())를 실행한다.

 

역참조 강제가 가변성과 상호작용하는 방법

 - T: Deref<Target=U>일때 &T에서 &U로 

     -> &T를 가지고 있고 T가 어떤 타입U에 대한 Deref를 구현했다면 명료하게 &U를 얻을 수 있음

 - T: DerefMut<Target=U>일때 &mut T에서 &mut U로

     -> 위에서 가변성 부분만 추가

 - T: Deref<Target=U>일때 &mut T에서 &U로

     -> 가변 참조자를 불변 참조자로 강제할 수 있음(불변참조자를 가변참조자로는 불가능)

https://rinthel.github.io/rust-lang-book-ko/ch15-02-deref.html

 

Deref 트레잇은 참조자를 통하여 데이터로의 접근을 허용합니다 - The Rust Programming Language

Deref 트레잇을 구현하는 것은 우리가 (곱하기 혹은 글롭 연산자와는 반대 측에 있는) 역참조 연산자 (dereference operator) * 의 동작을 커스터마이징 하는 것을 허용합니다. 스마트 포인터가 평범한

rinthel.github.io

 

Drop

 값이 스코프 밖으로 벗어나려고 할 때 사용

예를 들어 Box<T>는 Box가 가리키고 있는 heap의 공간을 해제하기 위해 Drop을 사용

Drop 트레잇을 이용하여 Scope밖으로 벗어났을 때 실행될 코드를 특정함.

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

pub fn run() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    let d = CustomSmartPointer { data: String::from("other stuff") };
    println!("CustomSmartPointers created.");
}

Drop트레잇을 구현한 CustomSmartPointer 구조체이다.

인스턴스가 run()의 스코프에서 벗어날 때 drop을 호출한다.

 

std::mem::drop을 이용하여 값을 일찍 버리기

값을 일찍 drop시켜야 될 경우가 있을 경우 사용한다.

pub fn run() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    drop(c);
    let d = CustomSmartPointer { data: String::from("other stuff") };
    println!("CustomSmartPointers created.");
}

 다음과 같이 사용하면 일찍 메모리 drop이 가능하다.

이는 std::mem::drop 메소드를 이용한 것으로 위에서 사용한 Drop트레잇과는 다르다.

https://rinthel.github.io/rust-lang-book-ko/ch15-03-drop.html

 

Drop 트레잇은 메모리 정리 코드를 실행시킵니다 - The Rust Programming Language

스마트 포인터 패턴에서 중요번 두 번째 트레잇은 Drop인데, 이는 값이 스코프 밖으로 벗어나려고 할 때 어떤 일이 발생될지를 커스터마이징하게끔 해줍니다. 우리는 어떠한 타입이 든 간에 Drop

rinthel.github.io

 

Rc<T> 참조 카운팅 스마트 포인터

복수 소유권을 허용하는 Rc<T> 타입이 있다.

복수 소유권이 필요하고, 어떤 부분이 그 데이터를 마지막으로 사용하게 될지 컴파일 타임에 알 수 없는 경우 Rc<T>를 사용한다.

부모 노드가 하나 있고 여러개의 자식 노드가 있는 트리형태의 자료구조에서 사용할 수 있을 것이다.

Rc<T>는 single thread 시나리오 상에서만 사용이 가능하며, multiple thread에서 사용하려면 별도의 설정이 필요하다.

 

Box를 사용했을 때 다음은 허용되지 않는다.

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

use List::{Cons, Nil};

pub fn run(){
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

b에서 a가 사용되었으므로 c에서는 a를 사용할 수 없다.(소유권 공유 비허용)

Box를 Rc로 바꾸어 작성해보자.

use std::rc::Rc;
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

pub fn run(){
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Rc를 사용하기 위해 use std::rc::Rc;를 호출해야 한다.

Box일때 a.clone()을 사용할 수도 있겠지만 Rust의 관례는 Rc::clone을 이용해야 한다.

Rc::clone은 clone과 달리 깊은 복사를 하지 않고 참조 카운트만 증가시키는데 clone에 비해 가볍다고 볼 수 있다.

Rc::strong_count를 이용하여 참조카운트를 출력시켜보면 다음과 같이 나타난다.

use std::rc::Rc;
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

pub fn run(){
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Rc::clone을 매번 호출할 때 마다 참조 카운트가 1씩 증가한다.

그리고 유효범위를 벗어날 경우 참조 카운트는 해제 되는것을 알 수 있다.

https://rinthel.github.io/rust-lang-book-ko/ch15-04-rc.html

 

Rc, 참조 카운팅 스마트 포인터 - The Rust Programming Language

대부분의 경우에서, 소유권은 명확합니다: 여러분은 어떤 변수가 주어진 값을 소유하는지 정확히 압니다. 그러나, 하나의 값이 여러 개의 소유자를 가질 수도 있는 경우가 있습니다. 예를 들면,

rinthel.github.io

 

RefCell<T>

 내부 가변성(interior mutability)은 unsafe를 사용하여 어떤 데이터에 대한 불변 참조자가 있을때 라도 데이터를 변형할  수 있도록 해주는 Rust의 디자인 패턴이다.

RefCell<T>는 내부 가변성 패턴을 따른다.

Rc<T>와는 다르게 RefCell<T>는 단일 소유권이다.

Box<T>와 다른점은 Box<T>는 빌림 규칙의 불변성은 컴파일 타임에 결정되지만 RefCell<T>를 이용하면 불변성은 런타임에 결정된다.

불변값 내부의 값을 변경하는 것을 내부 가변성 패턴이라 한다.

 

Rc<T> RefCell<T>를 조합하여 가변 데이터의 복수 소유자 만들기

Rc<T>는 어떤 데이터에 대해 복수의 소유자를 허용하지만, 그 데이터에 대한 불변 접근만 가능하다.

Rc<RefCell<T>>를 사용하면 변경이 가능한 복수의 소유자를 가지는 값을 가질 수 있다.

다음과 같이 사용할 수 있다.

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

pub fn run() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

https://rinthel.github.io/rust-lang-book-ko/ch15-05-interior-mutability.html

 

RefCell와 내부 가변성 패턴 - The Rust Programming Language

내부 가변성 (interior mutability) 은 어떤 데이터에 대한 불변 참조자가 있을 때라도 여러분이 데이터를 변형할 수 있게 해주는 러스트의 디자인 패턴입니다: 보통 이러한 동작은 빌림 규칙에 의해

rinthel.github.io

 

'Rust-Language' 카테고리의 다른 글

반복자, iterator  (0) 2021.09.03
Closure, 클로저 함수  (0) 2021.08.05
collection - vector  (0) 2021.04.23
if let 흐름 제어  (0) 2021.04.13
match 흐름 제어 연산자  (0) 2021.04.09
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함