ブランド付き型の実装 (ブランド化 3/4)

ブランド付き型の構築方法は、ブランドなしの型の構築方法とは異なります。

// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

use std::marker::PhantomData;

#[derive(Default)]
struct InvariantLifetime<'id>(PhantomData<*mut &'id ()>);
struct ProvenIndex<'id>(usize, InvariantLifetime<'id>);

struct Bytes<'id>(Vec<u8>, InvariantLifetime<'id>);

impl<'id> Bytes<'id> {
    fn new<T>(
        // このコンテキストで変更したいデータ。
        bytes: Vec<u8>,
        // `Bytes` のライフタイムに一意のブランドを付与する関数
        f: impl for<'a> FnOnce(Bytes<'a>) -> T,
    ) -> T {
        f(Bytes(bytes, InvariantLifetime::default()),)
    }

    fn get_index(&self, ix: usize) -> Option<ProvenIndex<'id>> {
        if ix < self.0.len() { Some(ProvenIndex(ix, InvariantLifetime::default())) }
        else { None }
    }

    fn get_proven(&self, ix: &ProvenIndex<'id>) -> u8 {
        debug_assert!(ix.0 < self.0.len());
        unsafe { *self.0.get_unchecked(ix.0) }
    }
}
  • 動機: ある型に対して「証明済みインデックス」を持たせたい一方で、それらの インデックスが同じ型の別の変数で使えてしまってほしくありません。また、それらの インデックスがスコープの外へ逃げてしまうことも避けたいです。

    今回のブランド付き型は Bytes: バイト配列です。

    今回のブランド付きトークンは ProvenIndex: 範囲内であることが分かっている インデックスです。

  • この実装には注目すべき点がいくつかあります:

    • newBytes を返さず、代わりに「開始データ」と、一度だけ使われ、 呼び出されたときに Bytes が渡されるクロージャを受け取ります。
    • その new 関数は、トレイト境界に for<'a> を持っています。
    • インデックスを取得する getter と、証明済みインデックスで値を取得する getter の両方があります。
  • 問い: なぜ newBytes を返さないのでしょうか?

    答え: Bytes には API が制御する一意なライフタイムを持たせる必要があるからです。

  • 問い: では、new()Bytes を返したらどうなるのでしょうか? 具体的にはどのような問題が 生じますか?

    答え: 仮にその new() メソッドがあるとして、そのシグネチャを考えてみてください:

    fn new<'a>() -> Bytes<'a> { ... }

    これでは API の利用者がライフタイム 'a を何にするか選べてしまい、異なる Bytes のインスタンス間のライフタイムが一意であり、互いにサブタイプ化 できないことを保証する能力が失われます。

  • 問い: なぜ get_indexget_proven の両方が必要なのでしょうか?

    想定される答え: 「あるインデックスが有効かどうかはコンパイル時には分からないから」

    問い: では、証明済みインデックスにはどんな意味があるのでしょうか?

    答え: 境界チェックを避けつつ、どのインデックスが有効かという知識を個々の 変数ごとに保持し、誤って別の変数に対して使えないようにするためです。

    注: 焦点は境界チェックの過剰な使用を避けることだけではなく、インデックスが 別の変数へ「またがって」使われるのを防ぐことにもあります。