blockの巣

const genericsの引数が条件を満たすときのみTraitを実装する

2022/03/10 22:31 公開
Rust

この記事のコードはv1.61.0-nightlyで確認しています。
またnightlyです。


何の話か

以下のような
ArrayWrapperというstructFooというtraitがあったとして

struct ArrayWrapper<const N: usize>([i32; N]);
trait Foo {
fn foo(&self) {}
}

以下のようにN < 5などの条件に当てはまるときだけFooを実装したい場合にどうすればよいかという話です。

ArrayWrapper([]).foo();              // OK
ArrayWrapper([0]).foo(); // OK
ArrayWrapper([0, 1]).foo(); // OK
ArrayWrapper([0, 1, 2]).foo() // OK
ArrayWrapper([0, 1, 2, 3]).foo() // OK
ArrayWrapper([0, 1, 2, 3, 4]).foo(); // Error

その前に

特定のNに対してFooを実装するには

impl Foo for ArrayWrapper<0> {}
fn main() {
ArrayWrapper([]).foo(); // OK
ArrayWrapper([0]).foo(); // Error
}

のようにしてやればよく、
すべてのNに対してFooを実装するには

impl<const N: usize> Foo for ArrayWrapper<N> {}
fn main() {
ArrayWrapper([]).foo(); // OK
ArrayWrapper([0]).foo(); // OK
ArrayWrapper([0, 1]).foo(); // OK
}

としてやればよいです。

whereで条件を追加してみる

以下のようにwhereNの条件を追加してみます。

impl<const N: usize> Foo for ArrayWrapper<N>
where N: {N < 5}, {}

するとコンパイルエラーが発生します。
そもそもwhere N: ...みたいな形式は構文として認められていないようです。

error: expected one of `!` or `::`, found `<`
--> src/main.rs:8:13
|
8 | where N: {N < 5}, {}
| - ^ - the item list ends here
| | |
| | expected one of `!` or `::`
| while parsing this item list starting here
error: expected item, found `,`
--> src/main.rs:8:17
|
8 | where N: {N < 5}, {}
| ^ expected item
error: could not compile `impl_trait_conditions_const_generics_argument` due to 2 previous errors

実装条件用のTraitを定義する

where N: ...のように書けないという問題は実装条件用のTraitを定義するという方法で解決できます。
ただし#![feature(generic_const_exprs)]の使用が必要です。

まず以下のようにFooCondition<const C: bool>というTraitを定義します。

trait FooCondition<const C: bool> {}

次にN < 5のときだけArrayWrapper<N>FooConditionを実装するようにします。

impl<const N: usize> FooCondition<{ N < 5 }> for ArrayWrapper<N> {}

最後にFooCondition<true>のときだけArrayWrapperFooを実装するようにします。

impl<const N: usize> Foo for ArrayWrapper<N> where ArrayWrapper<N>: FooCondition<true> {}

これでN < 5のときにArrayWrapper<N>Fooを実装するということができました。


コード全文

#![feature(generic_const_exprs)]
struct ArrayWrapper<const N: usize>([i32; N]);
trait Foo {
fn foo(&self) {}
}
trait FooCondition<const C: bool> {}
impl<const N: usize> FooCondition<{ N < 5 }> for ArrayWrapper<N> {}
impl<const N: usize> Foo for ArrayWrapper<N> where ArrayWrapper<N>: FooCondition<true> {}
fn main() {
ArrayWrapper([]).foo(); // OK
ArrayWrapper([0]).foo(); // OK
ArrayWrapper([0, 1]).foo(); // OK
ArrayWrapper([0, 1, 2]).foo(); // OK
ArrayWrapper([0, 1, 2, 3]).foo(); // OK
// ArrayWrapper([0, 1, 2, 3, 4]).foo(); // Error
}

#![feature(generic_const_exprs)]が必要な理由

stableではFooCondition<{ N < 5 }>のような条件式の中などでconst genericsの引数を使用できないという制限があるので
impl<const N: usize> FooCondition<{ N < 5 }> for ArrayWrapper<N> {}{ N < 5 }の部分のコンパイルを通すのに必要です。

#![feature(generic_const_exprs)]を使用する際の注意

warning: the feature `generic_const_exprs` is incomplete and may not be safe to use and/or cause compiler crashes
--> src/main.rs:1:12
|
1 | #![feature(generic_const_exprs)]
| ^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(incomplete_features)]` on by default
= note: see issue #76560 <https://github.com/rust-lang/rust/issues/76560> for more information

という警告が出ます。
ざっくり訳すと「generic_const_exprsは不完全で安全ではない可能性がありコンパイラがクラッシュすることがあります」という中々怖い内容です。
使うのであればそれを認識した上で使用しましょう。

あとがき

この機能を早くstableでも使えるようになってほしいです。

今回のコードはGitHubで公開しています。
stableで使えるようになったら更新すると思います。