🦀

【Rust Nightly】コンパイル時文字列型を作ってみた

に公開

nightly環境であれば

const fn foo<const N1: usize, const N2: usize>(a: [u8; N1], b: [u8; N2]) -> [u8; {N1 + N2}] {
    // 何かしらの処理
}

のようなジェネリック定数を計算できるのでコンパイル時文字列くらい作れるのではと思って試した。

結果

完成したコードがこちら。
使用している箇所は最後のmain関数。

#![feature(generic_const_exprs)]

struct ConstString<const N: usize> ([u8; N]);

impl<const N: usize> ConstString<N> {
    pub const fn new(s: &str) -> Self {
        let mut arr = [0; N];
        let mut i = 0;
        let bytes = s.as_bytes();
        while i < s.len() {
            arr[i] = bytes[i];
            i += 1;
        }
        Self (arr)
    }

    pub const fn len(&self) -> usize {
        self.0.len()
    }
}

macro_rules! const_string {
    ($name: ident = $s:expr)=> {
        const $name: ConstString::<{$s.as_bytes().len()}> = ConstString::<{$s.as_bytes().len()}>::new($s);
    };
}

const fn concat<const N1: usize, const N2: usize>(s1: ConstString::<N1>, s2: ConstString::<N2>) -> ConstString::<{ N1 + N2 }> {
    let mut arr = [0; N1 + N2];
    let mut i = 0;
    while i < N1 {
        arr[i] = s1.0[i];
        i += 1;
    }
    let mut j = 0;
    while j < N2 {
        arr[i] = s2.0[j];
        i += 1;
        j += 1;
    }
    ConstString ( arr )
}

impl<const N: usize> std::ops::Deref for ConstString<N> {
    type Target = str;

    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.0) }
    }
}

macro_rules! const_string_concat {
    ($name: ident = $s1:expr, $s2:expr)=> {
        const $name: ConstString::<{$s1.len() + $s2.len()}> = concat($s1, $s2);
    };
}

fn main() {
    use std::ops::Deref;
    const_string!(HELLO = "Hello");
    const_string!(SPACE = " ");
    const_string!(WORLD = "WORLD");
    const_string_concat!(HELLO_ = HELLO, SPACE);
    const_string_concat!(HELLO_WORLD = HELLO_, WORLD);
    println!("{}", &HELLO_WORLD.deref());
}

Rust Playgroundで試す

解説

文字列型

ConstString<const N: usize>([u8; N])

宣言の通り内部で配列を持っているだけ。長さはコンパイル時に決まる。
この型をそのまま使おうとすると

const HELLO: ConstString<5> = ConstString::<5>::new("Hello");

のように変数定義時に型の定数パラメーター(5の部分)をいちいち指定しなければならないため、次に説明するconst_stringマクロを使って変数を定義すると楽。

const_stringマクロ

const_string!(HELLO = "Hello");

のように使用することで定数HELLOを定義できる。
実装は引数の文字列の長さをそのまま定数パラメーターとして渡してやっているだけ。

macro_rules! const_string {
    ($name: ident = $s:expr)=> {
        const $name: ConstString::<{$s.as_bytes().len()}> = ConstString::<{$s.as_bytes().len()}>::new($s);
    };
}

結合

concat関数でConstString同士を結合できる。
ただしこれをそのまま使うと変数宣言と同じく

const HELLO: ConstString<5> = ConstString::new("Hello");
const WORLD: ConstString<5> = ConstString::new("World");
const HELLOWORLD: ConstString<10> = concat(HELLO, WORLD);

のように定数パラメーターを指定しなければならない。
のでconst_string!と同じようにマクロで結合できるようにしたのが次に説明するconst_string_concatマクロ。

const_string_concatマクロ

const_string_concat!(HELLO_ = HELLO, SPACE); // HELLOとSPACEを結合してHELLO_を定義
const_string_concat!(HELLO_WORLD = HELLO_, WORLD); //  HELLO_とWORLDを結合してHELLO_WORLDを定義

のように使用することで結合した文字列を定義できる。
実装は引数のConstStringの中身の長さを合計したものを定数パラメーターとして渡してやっている。

macro_rules! const_string_concat {
    ($name: ident = $s1:expr, $s2:expr)=> {
        const $name: ConstString::<{$s1.len() + $s2.len()}> = concat($s1, $s2);
    };
}

可変長引数で結合できるようにしたかったが考えるのを放棄した。再帰的にマクロを呼べばなんとかなりそうな気はしている。

その他

Derefを実装しているのは文字列として出力したかっただけで必須のものではない。

感想

実装前に思っていたよりは使い物になりそうなものができたが、使い所は思い付かない。

fn main() {
    use std::ops::Deref;
    const_string!(HELLO = "Hello");
    const_string!(SPACE = " ");
    const_string!(WORLD = "WORLD");
    const_string_concat!(HELLO_ = HELLO, SPACE);
    const_string_concat!(HELLO_WORLD = HELLO_, WORLD);
    println!("{}", &HELLO_WORLD.deref());
}

generic_const_exprsは早くstable入りしてほしい。

Discussion