Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

% カウント

置換を伴う反復

マクロ内で何かをカウントするのは、驚くほど厄介な作業です。最も単純な方法は、反復マッチで置換を使用することです。

macro_rules! replace_expr {
    ($_t:tt $sub:expr) => {$sub};
}

macro_rules! count_tts {
    ($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*};
}

fn main() {
    assert_eq!(count_tts!(0 1 2), 3);
}

これは小さめの数には問題のないアプローチですが、500 個程度のトークンを入力すると、おそらくコンパイラがクラッシュします。出力が次のようになることを考えてみてください。

0usize + 1usize + /* 約500個の `+ 1usize` */ + 1usize

コンパイラはこれを AST にパースする必要があり、その結果、実質的には 500 レベル以上の深さを持つ完全に不均衡な二分木が生成されます。

再帰

より古いアプローチは、再帰を使用することです。

macro_rules! count_tts {
    () => {0usize};
    ($_head:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)};
}

fn main() {
    assert_eq!(count_tts!(0 1 2), 3);
}

: rustc 1.2 の時点では、未知の型の大量の整数リテラルが推論される必要がある場合、コンパイラには深刻なパフォーマンス上の問題があります。ここではそれを避けるため、明示的に usize 型のリテラルを使用しています。

これが適切でない場合(型を置換可能にする必要がある場合など)は、as例: 0 as $ty, 1 as $ty など)を使うことで状況を改善できます。

これは動作しますが、容易に再帰制限を超えてしまいます。反復アプローチとは異なり、一度に複数のトークンにマッチすることで入力サイズを拡張できます。

macro_rules! count_tts {
    ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
     $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
     $_k:tt $_l:tt $_m:tt $_n:tt $_o:tt
     $_p:tt $_q:tt $_r:tt $_s:tt $_t:tt
     $($tail:tt)*)
        => {20usize + count_tts!($($tail)*)};
    ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
     $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
     $($tail:tt)*)
        => {10usize + count_tts!($($tail)*)};
    ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
     $($tail:tt)*)
        => {5usize + count_tts!($($tail)*)};
    ($_a:tt
     $($tail:tt)*)
        => {1usize + count_tts!($($tail)*)};
    () => {0usize};
}

fn main() {
    assert_eq!(700, count_tts!(
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        
        // 反復はこのあたり以降で壊れる
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
        ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
    ));
}

この特定の形式は、約 1,200 トークンまで動作します。

スライス長

3 つ目のアプローチは、スタックオーバーフローにつながらない浅い AST をコンパイラが構築できるようにすることです。これは、配列リテラルを構築し、len メソッドを呼び出すことで実現できます。

macro_rules! replace_expr {
    ($_t:tt $sub:expr) => {$sub};
}

macro_rules! count_tts {
    ($($tts:tt)*) => {<[()]>::len(&[$(replace_expr!($tts ())),*])};
}

fn main() {
    assert_eq!(count_tts!(0 1 2), 3);
}

これは 10,000 トークンまで動作することがテストされており、おそらくさらに大きくできます。欠点は、Rust 1.2 の時点では、これを定数式の生成に使用できないことです。結果は単純な定数に最適化できますが(デバッグビルドではメモリからのロードにコンパイルされます)、それでも定数位置(const の値や固定配列のサイズなど)では使用できません。

ただし、非定数のカウントで問題ない場合、これは非常に推奨される方法です。

Enum によるカウント

このアプローチは、相互に異なる識別子の集合をカウントする必要がある場合に使用できます。さらに、このアプローチの結果は定数として使用できます。

macro_rules! count_idents {
    ($($idents:ident),* $(,)*) => {
        {
            #[allow(dead_code, non_camel_case_types)]
            enum Idents { $($idents,)* __CountIdentsLast }
            const COUNT: u32 = Idents::__CountIdentsLast as u32;
            COUNT
        }
    };
}

fn main() {
    const COUNT: u32 = count_idents!(A, B, C);
    assert_eq!(COUNT, 3);
}

この方法には 2 つの欠点があります。第一に、上で示唆したように、有効な識別子(かつキーワードではないもの)しかカウントできず、それらの識別子の重複も許可されません。

第二に、このアプローチは hygienic ではありません。つまり、__CountIdentsLast の代わりに使用する何らかの識別子が入力として与えられると、enum 内のバリアントが重複するため、マクロは失敗します。