Rust Smart Pointer - Cow

이동훈·2021년 7월 16일
0

Rust 잡동사니

목록 보기
2/5

들어가며

이제 러스트를 사용한지 9개월 정도가 되가는 러스트 뉴비입니다. 러스트를 사용하면서 조금 더 효율적으로 러스트를 사용하고 싶어서 data structure에 대해 이것저것 찾아본 점이 많았는데 정리를 해두지 않으면 잊어버리기 쉬어서 이렇게 글로 남기려고 합니다. 물론 수요가 없는 공급이겠지만 배운 점은 항상 다른 사람과 나누라는 좋은 말이 있듯이 미래에 누군가를 위하여 간단히 정리해두려고 합니다. Rust official book에 개념들을 설명이 매우 잘되어있어서 이 시리즈에서는 개념적인 설명보다는 잡기술? 혹은 연관된 개념들? 쪽으로 글을 작성하려고 합니다~

이번 편은 Cow에 대하여 입니다.

Cow이란?

Cow는 Clone On Write의 의미를 가지고 있는 std::borrow 에서 제공되는 스마트 포인터입니다. Cow의 정의는 아래와 같습니다.

pub enum Cow<'a, B> 
where
    B: 'a + ToOwned + ?Sized, 
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

보시다시피 Cow란 Borrowed 와 Owned를 variant 라 가지고 있는 enum 입니다. 느낌이 오셨겠지만 Cow 는 reference 와 value 두개 모두를 encapsulate를 하고 있어서 만약 어떤 함수가 reference 와 value 모두를 받고 싶다면 매우 유용하게 사용할수 있는 스마트포인터입니다. 그러면 Cow는 어떤 상황에서 사용할까요?

아래는 rust official documentation 에 나온 예제입니다.

use std::borrow::Cow;

fn abs_all(input: &mut Cow<[i32]>) {
    for i in 0..input.len() {
        let v = input[i];
        if v < 0 {
            // Clones into a vector if not already owned.
            input.to_mut()[i] = -v;
        }
    }
}
// No clone occurs because `input` doesn't need to be mutated.
let slice = [0, 1, 2];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);

// Clone occurs because `input` needs to be mutated.
let slice = [-1, 0, 1];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);

// No clone occurs because `input` is already owned.
let mut input = Cow::from(vec![-1, 0, 1]);
abs_all(&mut input);

보시다시피 abs_all 함수는 array 안에 들어 있는 값들을 모두 절대값으로 변환해주는 함수입니다. 이 함수는 input array 를 변환할 필요가 있으면 input 을 clone 해서 소유해야 하지만 그럴 필요가 없을 경우 clone 할 필요가 없습니다. 즉, 런타임에서 dynamic 하게 clone 할지 말지를 정해야 하는 상황에서 Cow를 input 으로 소유하게 되면 간단하게 함수를 작성할수 있습니다. 여기서 clone은 input.to_mut() 에서 발생하게 되는데 to_mut()의 정의를 보게되면 아래와 같습니다.

pub fn to_mut(&mut self) -> &mut <B as ToOwned>::Owned {
        match *self {
            Borrowed(borrowed) => {
                *self = Owned(borrowed.to_owned());
                match *self {
                    Borrowed(..) => unreachable!(),
                    Owned(ref mut owned) => owned,
                }
            }
            Owned(ref mut owned) => owned,
        }
    }

즉, 만약에 Cow::Borrowed 상태에서는 self 를 Owned 로 바꾸어 주게 되면서 clone 이 발생합니다.

다른 Example

Official documentation 에 나온 예제 말고도 다른 예시를 한번 보면 좋을것 같습니다.

먼저 rust를 사용하면서 함수에서 input 을 String 으로 해야할지 &str 으로 해야할지 고민해보신 분이 적지 않으실 겁니다. Rust official documentation 에 따르면 callee 안에서 String 으로 conversion이 발생하는 경우 input 자체를 String으로 받고 그렇지 않을 경우에는 &str으로 받으라고 말을 해주고 있습니다. 그러나 만약 &str 혹은 String 둘다 input 으로 주어질수 있는 경우 어떻게 하는것이 좋을까요? 예를 한번 들어보죠.

fn fake_api_request() -> String {
	//some fake api request
  //returns name in String
  "Louis".to_string()
}

struct Name<'a> {
  inner: &'a str
}
impl<'a> Name<'a> {
  fn new(input: &'a str) -> Name<'a>{
    Name { inner: input}
  }
}

let first_name = Name::new("my_name");//compile successful!
let second_name = Name::new(fake_api_request()); //compile error!

이 간단한 코드는 Name struct를 만드는 코드입니다. Name struct 는 new 함수의 input 으로 &'a str을 받게 되는데 만약 first_name 처럼 값을 넣어주면 상관이 없지만 second_name 처럼 String 을 받는 경우 에러가 발생합니다. 즉, 이 경우에서 Name 을 generic 하게 만들기 위해서는 &str 과 String 모두를 input 을 받을 수 있어야 합니다. 이때 Cow 를 사용한다면 코드는 아래와 같이 바뀝니다.

struct Name<'a> {
  inner: Cow<'a, str>
}
impl<'a> Name<'a> {
  fn new(input: Cow<'a, str>) -> Name<'a>{
    Name { inner: input}
  }
}

let first_name = Name::new(Cow::Borrowed("my_name")); //compile successful!
let second_name = Name::new(Cow::Owned(fake_api_request())); //compile successful!

즉, Name 의 inner 를 Cow 를 사용해서 &str 과 String 모두 사용할수 있게 하고 나중에 이 inner 를 소유해야 한다면 as_mut() 를 불러서 lazy 하게 clone 할수 있습니다. 위에서 Cow::Borrowed("my_name") 가 이상해 보이신다고요? 다행히도 &str 과 String는 Cow에 대해서 Into 를 구현하고 있기 때문에 다음과 같이 사용할수 있습니다.

struct Name<'a> {
  inner: Cow<'a, str>
}
impl<'a> Name<'a> {
    pub fn new<S>(input: S) -> Name<'a>
        where S: Into<Cow<'a, str>>
    {
        Name { inner: input.into() }
    }
}

let first_name = Name::new("my_name"); //compile successful!
let second_name = Name::new(fake_api_request()); //compile successful!

더 깔끔하고 generic 한 api 가 완성이 되었습니다!

마치면서

이번글을 통해 Cow의 특성에 대해 알아보았습니다. 혹시나 글에 틀린 부분이 있다면 pandawithcat@gmail.com으로 연락주시면 감사하겠습니다~

References

https://jwilm.io/blog/from-str-to-cow/

https://deterministic.space/secret-life-of-cows.html

profile
개발이 어려운 개발자

2개의 댓글

comment-user-thumbnail
2022년 1월 4일

좋은 글 감사합니다! 👍

답글 달기
comment-user-thumbnail
2022년 1월 4일

중요하진 않지만 마지막에 as_mut()는 to_mut()의 오타일지도 모르겠네요! :)

답글 달기