-
스마트 포인터(Smart Pointer)는 포인터처럼 작동하지만 추가적인 메타데이터와 능력들도 가지고 있는 데이터 구조이다.
-
String
과Vec<T>
는 스마트 포인터이다. -
스마트 포인터는 보통 구조체를 이용하여 구현되어 있다. 스마트 포인터가 일반적인 구조체와 구분되는 특성은 스마트 포인터가
Deref
와Drop
트레잇을 구현한다는 것이다. -
Deref
트레잇은 스마트 포인터 구조체의 인스턴스가 참조자처럼 동작하도록 하여, 참조자나 스마트 포인터 둘 중 하나와 함께 작동하는 코드를 작성하게 해준다.-
스마트 포인터가 평범한 참조자처럼 취급될 수 있도록 구현한다.
-
Deref
를 구현한 구조체에 대해*
로 값을 참조하면,deref
함수를 호출한 후*
를 한번 호출하는 것으로 대치된다. (소유권 이전을 막기 위해 이런 식으로 구현되어있다.) -
Rust는 타입이 맞지 않는 경우 역참조를 강제하기 때문에 함수와 메소드 호출을 작성할 때
&
와*
를 이용한 명시적 참조 및 역참조를 생략할 수 있다.Deref::deref
를 구현하면 커스텀 구조체에서도 이것이 가능해진다. -
가변 참조자에 대한
*
를 오버라이딩하기 위해선DerefMut
트레잇을 사용해야한다. -
러스트는 다음의 세 가지 경우에 역참조 강제를 수행한다.
T: Deref<Target=U>
일때&T
에서&U
로T: DerefMut<Target=U>
일때&mut T
에서&mut U
로T: Deref<Target=U>
일때&mut T
에서&U
로
-
불변 참조자는 가변 참조자로 강제될 수 없다.
-
-
Drop
트레잇은 스마트 포인터의 인스턴스가 스코프 밖으로 벗어났을 때 실행되는 코드를 커스터마이징 가능하도록 해준다.-
파일이나 네트워크 연결 같은 자원을 해제하는 데에 사용될 수도 있다.
-
변수들은 만들어진 순서의 역순으로 Drop된다.
-
Double free 문제가 생길 수 있기 때문에
drop()
함수를 직접 호출하는 것은 허용되지 않는다. 대신std::mem::drop
를 사용하여 메모리에서 직접 지울 수 있다.
-
-
스마트 포인터는 Rust에서 자주 활용되는 패턴이므로, 직접 비슷한 구조로 구현할 수 있다.
대표적인 스마트 포인터
- 표준 라이브러리의 대표적인 스마트 포인터들에 대해 알아보자.
Box<T>
값을 힙에 할당하기 위한 -
Box<T>
는 데이터를 스택이 아니라 힙에 저장할 수 있도록 해준다. -
아래와 같은 상황에서 사용할 수 있다.
- 컴파일 타임에 크기를 알 수 없는 타입을 갖지만, 사이즈를 아는 상태에서 해당 타입의 값을 이용하고 싶을 때
- 커다란 데이터의 데이터를 복사하지 않고 소유권을 옮기기를 원할 때
- 박스 안의 힙에 큰 데이터를 저장하면, 작은 양의 포인터 데이터만 스택 상에서 복사되고 데이터는 힙의 한 곳에 머물게 된다.
- 어떤 값의 소유와 타입에 관계없이 특정 트레잇을 구현한 타입이라는 점만 신경 쓰고 싶을 때
-
new를 사용해서 생성할 수 있다.
- 박스는 재귀적 타입을 가능하게 한다.
-
현재 아이템의 값과 다음 아이템을 저장하는 Pair(Cons)가 있다고 해보자.
-
Nil
을 값으로 가지게 함으로써 Cons가 계속해서 이어지는 걸 막을 수 있지만, 컴파일러가 variants를 계산할 때는 무한대의 메모리가 필요한 타입인 것으로 해석한다. 따라서 위의 코드는 실행할 수 없다. -
Box<T>
를 사용하면 무한한 variants를 가졌던 위와 달리, 한정된 크기를 가지는 pointer 값만을 가지기 때문에 문제가 해결된다.
-
Rc<T>
복수개의 소유권을 가능하게 하는 참조 카운팅 타입 -
대부분의 경우에서, 소유권은 한 변수당 하나로 명확하다.
-
하지만 하나의 값이 여러 개의 소유자를 가질 수도 있는 경우가 있다.
- 예를 들면, 그래프 데이터 구조에서, 여러 에지가 동일한 노드를 가리킬 수 있다. 그 노드는 개념적으로 해당 노드를 가리키는 모든 에지들에 의해 소유된다. 노드는 어떠한 에지도 이를 가리키지 않을 때까지는 메모리 정리가 되어서는 안된다.
-
복수 소유권을 가능하게 하기 위해서, 러스트는
Rc<T>
라는 타입을 가지고 있다. Reference Counting의 약자이고, 이 타입은 어떤 값이 계속 사용되는지 혹은 그렇지 않은지를 알기 위해 해당 값에 대한 참조자의 갯수를 계속 추적한다. -
만일 값에 대한 참조자가 0개라면, 그 값은 어떠한 참조자도 무효화하지 않고 메모리에서 정리될 수 있다.
-
프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶고, 어떤 부분이 그 데이터를 마지막에 이용하게 될지 컴파일 타임에 알 수 없는 경우
Rc<T>
타입을 사용한다. -
Rc<T>
는 오직 단일 스레드 상에서만 사용 가능하다. -
예제를 살펴보자.
-
이러한 구조를 나타내는 코드가 있다.
-
a
에 대한 소유권을 두 군데에 지정하는 것이기 때문에 위 코드는 컴파일 되지 않는다. (참조자를 사용하는 경우 a가 사라지지 않는다는 것을 보장할 수 없다.) -
Box<T>
의 자리에Rc<T>
를 이용하여 정의하면 이러한 문제를 해결할 수 있다.
Rc::clone
의 호출은 오직 참조 카운트만 증가시킨다.Rc::strong_count
함수를 호출함으로써 카운트 값을 얻을 수 있다.
Rc<T>
는 읽기 전용으로 우리 프로그램의 여러 부분 사이에서 데이터를 공유하도록 허용해준다.
RefCell<T>
를 통해 접근 가능한 Ref와 RefMut
빌림 규칙을 컴파일 타임 대신 런타임에 강제하는 타입인, -
Rc<T>
와는 다르게,RefCell<T>
타입은 가지고 있는 데이터 상에 단일 소유권을 나타낸다. -
Box<T>
는 하나의 가변 참조자 혹은 임의 개수의 불변 참조자를 가질 수 있고 항상 유효해야 한다는 것을 컴파일러가 강하게 제약한다. -
하지만
RefCell<T>
는 런타임에 검사된 가변 빌림을 허용하기 때문에,RefCell<T>
이 불변일 때라도RefCell<T>
내부의 값을 변경할 수 있다. 즉, 코드가 빌림 규칙을 따르는 것을 확신하지만 컴파일러는 이를 이해하고 보장할 수 없을 경우 유용하게 쓰인다. -
이 코드는 Rust에 의해 컴파일될 수 없다. (cannot borrow immutable local variable
x
as mutable) -
RefCell<T>
를 사용하면 아래와 같이 수정할 수 있다. -
컴파일러 내의 빌림 검사기는 이러한 내부 가변성을 허용하는 대신 런타임에 검사를 수행한다. 만약 규칙이 위반된다면 컴파일러 에러 대신
panic!
을 얻을 것이다. -
RefCell<T>
또한 단일 스레드 상에서만 사용 가능하다.
Rc<T>
와 RefCell<T>
의 조합
-
Rc<T>
는 어떤 데이터에 대해 복수의 소유자를 허용하지만, 그 데이터에 대한 불변 접근만 제공한다.- 그러므로 만약
RefCell<T>
을 들고 있는Rc<T>
를 선언한다면, 변경 가능하면서 복수의 소유자를 갖는 값을 가질 수 있다!
- 그러므로 만약
-
어떤 리스트의 소유권을 공유하는 여러 개의 리스트를 가질 수 있도록 하기 위해
Rc<T>
를 사용했던 예제를 떠올려보자.-
이전에는
Rc<T>
가 불변의 값만을 가질 수 있었다. -
이 리스트 안의 값을 변경할 수 있게 하기 위해서
RefCell<T>
를 사용할 수 있다.
-
참고