Progrust Library.

// dictionary

演算子オーバーロード.

演算子オーバーロードは、+や!といった演算子の振る舞いをトレイトの実装として定義し、自作の型でも演算子を使えるようにする仕組みです。演算子はそれぞれ標準ライブラリのstd::opsにあるトレイトと対応づけられていて、a + bという式はAddトレイトのaddというメソッドの呼び出しとして扱われます1。

オーバーロードできるのは裏付けとなるトレイトを持つ演算子だけです。トレイトのない=(代入)の意味は変えられませんし、新しい演算子を作る手段も用意されていません(構文そのものを増やしたい場合はマクロを使うことになります)2。

use std::ops::Add;

// 税込価格(円)
#[derive(Debug, Clone, Copy)]
struct Price(u32);

impl Add for Price {
    type Output = Price; // 加算した結果の型

    fn add(self, rhs: Price) -> Price {
        Price(self.0 + rhs.0)
    }
}

fn main() {
    let book = Price(3200);
    let coffee = Price(480);
    println!("{:?}", book + coffee); // Price(3680)
    println!("{:?}", book + coffee + coffee); // Price(4160)
}
Playgroundで開く

演算子と対応するトレイト

オーバーロードできるのは数値演算で使う算術演算子だけではなく、ビット演算・添字アクセス・参照外しなどもstd::opsのトレイトに対応づけられています2。複合代入(+=など)は+とは独立したトレイトが担当するため、Addを実装しただけでは+=は使えるようになりません。

演算子とstd::opsトレイトの対応表
演算子 トレイト 複合代入のトレイト
+・-・*・/・% Add・Sub・Mul・Div・Rem AddAssign〜RemAssign
&・|・^・<<・>> BitAnd・BitOr・BitXor・Shl・Shr BitAndAssign〜ShrAssign
-(符号反転)・!(否定) Neg・Not —
[](添字アクセス) Index・IndexMut —
*(参照外し) Deref・DerefMut —
()(関数呼び出し) Fn・FnMut・FnOnce —

このうち()に対応するFn・FnMut・FnOnceだけは、自作の型に手で実装することがまだできません(Rust 1.93時点。試みるとエラー: E0183になります)。クロージャや関数がコンパイラによって自動的に実装するトレイト、という位置づけです3。

Outputと右辺の型

std::opsのトレイトは、演算した結果の型をOutputという関連型で表します。Addの宣言は次の形です4。

// std::ops::Add の宣言(抜粋)
trait Add<Rhs = Self> {
    type Output; // 演算結果の型
    fn add(self, rhs: Rhs) -> Self::Output;
}

型引数Rhsは右辺の型で、既定値がSelfのため、省略すると同じ型同士の演算になります。RhsとOutputを変えれば、異なる型との演算や、被演算子とは別の型を返す演算も定義できます。

use std::ops::Mul;

#[derive(Debug, Clone, Copy)]
struct Price(u32);

// 価格 × 個数 → 価格(右辺は u32、結果は Price)
impl Mul<u32> for Price {
    type Output = Price;

    fn mul(self, count: u32) -> Price {
        Price(self.0 * count)
    }
}

fn main() {
    println!("{:?}", Price(120) * 3); // Price(360)
}
Playgroundで開く

ジェネリクスを使うコードで演算子に頼るときは、T: Add<Output = T>のようにトレイト境界の中でOutputまで指定します。結果の型が決まらないと、加算した値を何に使えるか分からないためです。

オーバーロードできない演算子

&&と||には対応するトレイトがなく、オーバーロードできません。左辺だけで結果が決まるなら右辺を評価しないという短絡評価の性質が、両辺の値を受け取る他の演算子トレイトの設計と噛み合わないためで、std公式ドキュメントでは「設計を検討中」とされています2。そのため&&・||は論理値型専用のままです。論理演算子のうち!(Not)や、短絡評価しない&・|・^(BitAnd・BitOr・BitXor)はオーバーロードできるので、対象外なのは&&・||の2つだけ、ということになります。

このほか、次の演算子もオーバーロードの対象外です。

  • =(代入)— 裏付けとなるトレイトがありません2
  • &・&mut(借用演算子)— The Rust Referenceに「オーバーロードできない」と明記されています5。ビット演算の&とは別物である点に注意してください
  • as(キャスト)— std::opsにキャストを担うトレイトが存在しません2。自作の型に型変換を持たせたい場合は、変換トレイトのFrom・Intoを実装して.into()で変換します

比較演算子はstd::cmpが担う

比較演算子(==や<)もオーバーロードできますが、対応するトレイトはstd::opsではなくstd::cmpのPartialEq・PartialOrdです6。4つのトレイトの関係は比較のトレイトで扱っています(多くの場合はderive属性で導出できます)。

オペランドの受け取り方にも違いがあります。算術演算子のオペランドは値として評価されるため、Copyトレイトを実装していない型では演算のたびにムーブが起きます7。一方、比較演算子はオペランドを共有借用で受け取り、a == bはPartialEq::eq(&a, &b)に展開されるため、比較しても値は失われません6。

補足

+=にはAddAssignが別に必要

+と+=は別のトレイトです。Addだけを実装した型に+=を使うとエラー: E0368になります。

use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Price(u32);

impl Add for Price {
    type Output = Price;
    fn add(self, rhs: Price) -> Price {
        Price(self.0 + rhs.0)
    }
}

fn main() {
    let mut total = Price(0);
    total += Price(120); // エラー: E0368(AddAssignを実装していない)
    println!("{:?}", total);
}
Playgroundで開く

AddAssignを実装すると+=が使えるようになります。プリミティブ型以外ではa += bはa.add_assign(b)と同じで8、add_assignが&mut selfを取るため、左辺をムーブせずその場で書き換えます。

use std::ops::AddAssign;

#[derive(Debug)]
struct Price(u32);

impl AddAssign for Price {
    fn add_assign(&mut self, rhs: Price) {
        self.0 += rhs.0;
    }
}

fn main() {
    let mut total = Price(0);
    total += Price(120);
    total += Price(480);
    println!("{:?}", total); // Price(600)
}
Playgroundで開く
オペランドがムーブされる問題

Add::addはselfを値で受け取るため、Copyを実装していない型ではa + bのあとにaが使えなくなります。標準ライブラリの文字列型の+もこの形で、impl Add<&str> for Stringとして左辺を値で、右辺を文字列スライスへの参照で受け取ります。

std公式ドキュメントは、自作の型が加算をサポートするならTだけでなく&TにもAddを実装しておくことを勧めています。そうすれば、型引数を使う汎用的なコードから呼ぶときに毎回クローンせずに済むためです2。標準ライブラリの数値型もこの形の実装を持っており、&3 + &4のように参照同士でも加算できます。

外部の型にも実装できる場合がある

std::opsのトレイトは外部(std)のトレイトなので、実装できる範囲はトレイトのオーファンルールに従います。ただし制約は「トレイトか、実装に現れる型の少なくとも1つが自分のクレートで定義されていること」なので、型引数のどこかに自分の型が現れていればu32のような外部の型にも実装できます。3 * Price(120)のように自分の型を右辺に置く演算を書きたいときは、この形の実装を追加します。

use std::ops::Mul;

#[derive(Debug, Clone, Copy)]
struct Price(u32);

// 外部の型 u32 への実装。型引数に自分の型 Price が現れているため書ける
impl Mul<Price> for u32 {
    type Output = Price;

    fn mul(self, price: Price) -> Price {
        Price(self * price.0)
    }
}

fn main() {
    println!("{:?}", 3 * Price(120)); // Price(360)
}
Playgroundで開く
演算子トレイトはderiveできない

PartialEqなどとは違い、AddやNotを#[derive(...)]で導出することはできません。フィールドごとに加算すればよいのか、片方のフィールドだけを足すのかは型ごとに違い、機械的には決められないためです。

処理系が組み込みで用意しているderiveはClone・Copy・Debug・Default・Eq・Hash・Ord・PartialEq・PartialOrdの9つだけで9、いずれも振る舞いがフィールドから一意に決まるものです(derive属性)。std::opsのトレイトは1つも含まれていないため、演算子の中身は必ず手で書きます。

Footnotes

  1. The Rust Reference - Operator expressions “Operators are defined for built in types by the Rust language.” / “Many of the following operators can also be overloaded using traits in std::ops or std::cmp.” ↩

  2. std公式ドキュメント — Module std::ops 同ページのトレイト一覧にas(キャスト)に対応するトレイトは存在しない。“Only operators backed by traits can be overloaded. For example, the addition operator (+) can be overloaded through the Add trait, but since the assignment operator (=) has no backing trait, there is no way of overloading its semantics. Additionally, this module does not provide any mechanism to create new operators. If traitless overloading or custom operators are required, you should look toward macros to extend Rust’s syntax.” / “Implementations of operator traits should be unsurprising in their respective contexts, keeping in mind their usual meanings and operator precedence. For example, when implementing Mul, the operation should have some resemblance to multiplication (and share expected properties like associativity).” / “Note that the && and || operators are currently not supported for overloading. Due to their short circuiting nature, they require a different design from traits for other operators like BitAnd. Designs for them are under discussion.” / “Many of the operators take their operands by value. … using these operators in generic code, requires some attention if values have to be reused as opposed to letting the operators consume them. One option is to occasionally use clone. Another option is to rely on the types involved providing additional operator implementations for references. For example, for a user-defined type T which is supposed to support addition, it is probably a good idea to have both T and &T implement the traits Add<T> and Add<&T> so that generic code can be written without unnecessary cloning.” 演算子とトレイトの対応表も同ページ(Rust 1.93のstdソースcore/src/ops/mod.rsで確認)。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  3. std公式ドキュメント — Trait std::ops::Fn 必須メソッドcallには “🔬 This is a nightly-only experimental API. (fn_traits #29625)” と付いており、安定版では自作の型に手で実装できない。Rust 1.93でimpl FnOnce<(u32,)> for 自作型を書くと “error[E0183]: manual implementations of FnOnce are experimental” になる。 ↩

  4. std公式ドキュメント — Trait std::ops::Add 定義はpub trait Add<Rhs = Self>、必須の関連型type Outputと必須メソッドfn add(self, rhs: Rhs) -> Self::Output(Rust 1.93のstdソースcore/src/ops/arith.rsで確認)。 ↩

  5. The Rust Reference - Borrow operators “These operators cannot be overloaded.” ↩

  6. The Rust Reference - Comparison operators 対応表で==・!=がstd::cmp::PartialEq::eq・neに、<・<=・>・>=がstd::cmp::PartialOrdのメソッドに対応づけられている。“Unlike the arithmetic and logical operators above, these operators implicitly take shared borrows of their operands, evaluating them in place expression context” / “a == b; is equivalent to ::std::cmp::PartialEq::eq(&a, &b);” / “This means that the operands don’t have to be moved out of.” ↩ ↩2

  7. The Rust Reference - Arithmetic and logical binary operators “The operands of all of these operators are evaluated in value expression context so are moved or copied.” 演算子とstd::opsトレイト・複合代入トレイトの対応表も同節。 ↩

  8. The Rust Reference - Compound assignment expressions “this expression is syntactic sugar for using the corresponding trait for the operator … and calling its method with the left hand side as the receiver and the right hand side as the next argument.”(x += y;とx.add_assign(y);が等価であることが例示されている) ↩

  9. The Rust Reference - The derive attribute “The list of built-in derives are: Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd” ↩

この辞書が使われているページ

backlinks 6

  1. 辞書トレイト
  2. 辞書数値演算
  3. 辞書比較のトレイト
  4. 辞書比較演算子
  5. 辞書論理演算子
  6. 本動かして学ぶRustプログラミング問題集 › 第21章 トレイトオブジェクトと関連型