LoginSignup
11
8

More than 3 years have passed since last update.

Rust勉強中 - その18 -> 演算子オーバーロード

Last updated at Posted at 2019-10-22

自己紹介

出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。

Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。

環境

新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.38.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell

前回

前回はトレイトについて学びました。
Rust勉強中 - その17

演算子オーバーロード

演算子オーバーロードは、定義した型に対して、算術や比較などの演算を実装できる機能です。
演算子オーバーロードを行うには、対象の演算トレイトを型に実装します。
例えばx + yという式を呼び出したとき、実はx.add(y)というAddトレイトのaddメソッドを呼び出していることになります。

以降、単項演算子から順番に型への実装方法を学んでいきます。

単項演算子

トレイト
$-x$ std::ops::Neg
$!x$ std::ops::Not

std::ops::Negの実装をみてみます。

pub trait Neg {
    type Output;
    fn neg(self) -> Self::Output;
}

Negトレイトにnegメソッドがあります。Negをサポートする型にはnegメソッドを実装する必要があります。Outputは関連型でnegメソッドの出力の型を指定します。Notトレイトも同じように定義されています。

では、Negトレイトを実装する例を以下に示します。

#[derive(Debug)]
struct S<T, U> {
    v: Vec<T>,
    w: U
}

use std::ops::Neg;
impl<T, U> Neg for S<T, U> 
    where U: Neg<Output=U> 
{
    type Output = S<T, U>;
    fn neg(self) ->  Self::Output {
        S {v: self.v, w: -self.w}
    }
}

fn main() {
    let s = S {v: vec![0, 1, 2], w: 0.5};
    // println!("{:?}", s.neg()); // S { v: [0, 1, 2], w: -0.5 }
    println!("{:?}", -s);      // S { v: [0, 1, 2], w: -0.5 }
}

まず、VecとUをフィールドに持つ構造体を定義します。
次に、useでNegをインポートします。演算子オーバーロードを行う際には、対象のトレイトをスコープに入れておく必要があります。
implでSにNegを実装します。この時、ジェネリックimplブロックでT型とU型を受け取ることを明示しておきます。今回はUだけNegを適用させるために(Vecをネガティブにする方法が分かりませんでした)、whereでUがNegの制約を受けるようにしています。UのNegのOutputは同じU型です。
メソッド定義では、negメソッドでselfを受け取って、Self::Output(つまり、S)を返しています。
最後にmainで実行して結果を確認しています。
こんな感じでダラダラ説明を書いているのは、ここら辺が私自身曖昧なので、トレイトの復習と頭を整理するために書いています、ご了承ください。

この時、mainでは変数sの所有権をnegメソッドに渡しているため、メソッド実行後は変数sにアクセスできません。そこで、変数sの参照をnegに渡せるようなメソッドを実装する必要があります。

impl<T, U> Neg for &S<T, U> 
    where T: Clone, U: Neg<Output=U>+Copy
{
    type Output = S<T, U>;
    fn neg(self) ->  Self::Output {
        S {v: self.v.clone(), w: -self.w}
    }
}

fn main() {
    let s = S {v: vec![0, 1, 2], w: 0.5};
    // println!("{:?}", s.neg()); // S { v: [0, 1, 2], w: -0.5 }
    // println!("{:?}", -s); // S { v: [0, 1, 2], w: -0.5 }
    println!("{:?}", (&s).neg()); // S { v: [0, 1, 2], w: -0.5 }
    println!("{:?}", -&s); // S { v: [0, 1, 2], w: -0.5 }
}

implブロックのforで参照Sについて実装すると宣言しています。こうすることで、negメソッドに渡されるself引数が参照であるということになります。whereではTがCloneの、UがCopyの制約を受けるようにする必要がありました。この理由はnegメソッドの引数は参照を受けており、新たな構造体Sを出力します。新たな構造体Sが生きている間に、selfの参照先のvやwがドロップされても良いように、vのCloneとwのCopyをすべきだということなんだと思います。もしかしたら、もっと別の良い方法があるかもしれません。

二項演算子

トレイト
$x+y$ std::ops::Add
$x-y$ std::ops::Sub
$x*y$ std::ops::Mul
$x/y$ std::ops::Div
$x\%y$ std::ops::Rem
$x\&y$ std::ops::BitAnd
$x|y$ std::ops::BitOr
$x$^$y$ std::ops::BitXor
$x<<y$ std::ops::Shl
$x>>y$ std::ops::Shr

AndトレイトとShlトレイトの定義は以下のようになっています。

pub trait Add<RHS = Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}
pub trait Shl<RHS> {
    type Output;
    fn shl(self, rhs: RHS) -> Self::Output;
}

AddではRHSの型指定を省略することができます。一方、ShlではRHSを明示的に型指定をします。なぜなら、シフトの右辺値はSelfと同じになるとは限らないからです。
以下にAddの実装例を示します。

...
use std::ops::Add;
impl<T, U> Add for &S<T, U>
    where T: Clone, U: Add<Output=U>+Copy
{
    type Output = S<T, U>;
    fn add(self, rhs: Self) -> Self::Output {
        let mut tmp_v = self.v.clone();
        tmp_v.append(&mut rhs.v.clone());
        S {v: tmp_v,  w: self.w+rhs.w}
    }
}

fn main() {
    ...
    let mut s2 = S {v: vec![3, 4], w: 10.25};
    println!("{:?}", &s+&s2);    // S { v: [0, 1, 2, 3, 4], w: 10.75 }
    println!("{:?}, {:?}", &s, &s2); // S { v: [0, 1, 2], w: 0.5 }, S { v: [3, 4], w: 10.25 }
}

まだ不慣れなので、やたら散らかってしまった感じです。
まず、implブロックですがこれは先ほどの参照バージョンと同じようにforに参照構造体Sを指定しました。
そして、addメソッドの定義では、引数rhsの型にSelfを指定しています。このメソッドの動作はvのベクトル同士を結合して、wは足し算を行うようにしました。

複合代入演算子

トレイト
$x+=y$ std::ops::AddAssign
$x-=y$ std::ops::SubAssign
$x*=y$ std::ops::MulAssign
$x/=y$ std::ops::DivAssign
$x\%=y$ std::ops::RemAssign
$x\&=y$ std::ops::BitAndAssign
$x|=y$ std::ops::BitOrAssign
$x$^=$y$ std::ops::BitXorAssign
$x<<=y$ std::ops::ShlAssign
$x>>=y$ std::ops::ShrAssign

AddAssignトレイトの定義は以下のようになっています。

pub trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, Rhs);
}

selfは可変参照を受け取るようです。関連型のOutputはありません。
AddAssignの実装例を以下に示します。

...
use std::ops::AddAssign;
impl<T, U> AddAssign<&S<T, U>> for S<T, U>
    where T: Clone, U: AddAssign<U>+Copy
{
    fn add_assign(&mut self, rhs: &Self) {
        self.v.append(&mut rhs.v.clone());
        self.w += rhs.w
    }
}



fn main() {
    let mut s = S {v: vec![0, 1, 2], w: 0.5};
    ...
    s += &s2;
    println!("{:?}", s);    // S { v: [0, 1, 2, 3, 4], w: 10.75 }
}

これは、先ほどのAddトレイトの実装をself自身に上書きするようにしたバージョンです。説明は省略します。

等価判定演算子

トレイト
$x==y$ std::cmp::PartialEq
$x!=y$ std::cmp::PartialEq

PartialEqトレイトの定義は以下のようになっています。

pub trait PartialEq<Rhs = Self> where Rhs: ?Sized {
    fn eq(&self, other: &Rhs) -> bool;

    fn ne(&self, other: &Rhs) -> bool { ... }
}

PartialEqにはeqメソッドとneメソッドがあります。neメソッドはデフォルトメソッドなので実装してもしなくても良いです。whereで?Sizedとなっていますが今回はスルーします。

以下に実装例を示します。

...
use std::cmp::PartialEq;
impl<T, U> PartialEq for S<T, U>
    where T: PartialEq, U: PartialEq
{
    fn eq(&self, other: &Self) -> bool {
        self.v==other.v && self.w==other.w
    }
}

fn main() {
    ...
    println!("{}", &s==&s);  // true
    println!("{}", &s==&s2); // false
}

PartialEqを継承したEqがあります。
PartialEqは部分同値関係の場合に実装します。Eqは完全同値関係の場合にPartialEqとともに明示的に実装します。f32とf64は部分同値関係です。

順序比較演算子

トレイト
$x<y$ std::cmp::PartialOrd
$x>y$ std::cmp::PartialOrd
$x<=y$ std::cmp::PartialOrd
$x>=y$ std::cmp::PartialOrd

PartialOrdの定義は以下のようになっています。

pub trait PartialOrd<Rhs = Self>: PartialEq<Rhs> where Rhs: ?Sized {
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;

    fn lt(&self, other: &Rhs) -> bool { ... }
    fn le(&self, other: &Rhs) -> bool { ... }
    fn gt(&self, other: &Rhs) -> bool { ... }
    fn ge(&self, other: &Rhs) -> bool { ... }
}

PartialOrdはPartialEqを継承しています。実装する必要があるメソッドは、partial_cmpのみです。これは、OptionのOrderingを返します。Orderingは以下のenumで定義されています。

pub enum Ordering {
    Less,
    Equal,
    Greater,
}

実装例を以下に示します。

use std::cmp::PartialOrd;
use std::cmp::Ordering;
impl<T, U> PartialOrd for S<T, U>
    where T: PartialOrd, U: PartialOrd
{
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.v.len()==other.v.len() {
            Some(Ordering::Equal)
        } else if self.v.len()>other.v.len() {
            Some(Ordering::Greater)
        } else if self.v.len()<other.v.len() {
            Some(Ordering::Less)
        } else {
            None
        }
    }
}

fn main() {
    ...
    println!("{:?}", s==s2); // false
    println!("{:?}", s>s2); // true
    println!("{:?}", s<s2); // false
}

構造体Sのフィールドvの長さを比較することにしました。Ordering列挙型を使っているので、インポートしています。

インデックス

トレイト
x[i] std::ops::Index
&mut x[i] std::ops::IndexMut

IndexとIndexMutの定義は以下です。

pub trait Index<Idx> where Idx: ?Sized {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}
pub trait IndexMut<Idx>: Index<Idx> where Idx: ?Sized {
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}

IndexMutはIndexを継承しています。Indexはselfを参照で受け取り、IndexMutはselfを可変参照で受け取っています。Idxは明示的に型指定する必要があります。
IndexとIndexMutの実装例を以下に示します。

use std::ops::{
    Index, 
    IndexMut
};
impl<T, U> Index<usize> for S<T, U>
{
    type Output = T;
    fn index(&self, index: usize) -> &Self::Output {
        &self.v[index]
    }
}
impl<T, U> IndexMut<usize> for S<T, U>
{
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        &mut self.v[index]
    }
}

fn main() {
    ...
    println!("{}", s[0]); // 0
    s[0] = 1;
    println!("{}", s[0]); // 1
}

index_mutは可変参照なので変更ができます。

ソース

#[derive(Debug)]
struct S<T, U> {
    v: Vec<T>,
    w: U
}

use std::ops::Neg;
impl<T, U> Neg for S<T, U> 
    where U: Neg<Output=U>
{
    type Output = S<T, U>;
    fn neg(self) ->  Self::Output {
        S {v: self.v, w: -self.w}
    }
}

impl<T, U> Neg for &S<T, U> 
    where T: Clone, U: Neg<Output=U>+Copy
{
    type Output = S<T, U>;
    fn neg(self) ->  Self::Output {
        S {v: self.v.clone(), w: -self.w}
    }
}

use std::ops::Add;
impl<T, U> Add for &S<T, U>
    where T: Clone, U: Add<Output=U>+Copy
{
    type Output = S<T, U>;
    fn add(self, rhs: Self) -> Self::Output {
        let mut tmp_v = self.v.clone();
        tmp_v.append(&mut rhs.v.clone());
        S {v: tmp_v,  w: self.w+rhs.w}
    }
}

use std::ops::AddAssign;
impl<T, U> AddAssign<&S<T, U>> for S<T, U>
    where T: Clone, U: AddAssign<U>+Copy
{
    fn add_assign(&mut self, rhs: &Self) {
        self.v.append(&mut rhs.v.clone());
        self.w += rhs.w
    }
}

use std::cmp::PartialEq;
impl<T, U> PartialEq for S<T, U>
    where T: PartialEq, U: PartialEq
{
    fn eq(&self, other: &Self) -> bool {
        self.v==other.v && self.w==other.w
    }
}

use std::cmp::PartialOrd;
use std::cmp::Ordering;
impl<T, U> PartialOrd for S<T, U>
    where T: PartialOrd, U: PartialOrd
{
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.v.len()==other.v.len() {
            Some(Ordering::Equal)
        } else if self.v.len()>other.v.len() {
            Some(Ordering::Greater)
        } else if self.v.len()<other.v.len() {
            Some(Ordering::Less)
        } else {
            None
        }
    }
}

use std::ops::{
    Index, 
    IndexMut
};
impl<T, U> Index<usize> for S<T, U>
{
    type Output = T;
    fn index(&self, index: usize) -> &Self::Output {
        &self.v[index]
    }
}
impl<T, U> IndexMut<usize> for S<T, U>
{
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        &mut self.v[index]
    }
}

fn main() {
    let mut s = S {v: vec![0, 1, 2], w: 0.5};
    // println!("{:?}", s.neg()); // S { v: [0, 1, 2], w: -0.5 }
    // println!("{:?}", -s); // S { v: [0, 1, 2], w: -0.5 }
    println!("{:?}", (&s).neg()); // S { v: [0, 1, 2], w: -0.5 }
    println!("{:?}", -&s); // S { v: [0, 1, 2], w: -0.5 }
    let s2 = S {v: vec![3, 4], w: 10.25};
    println!("{:?}", &s+&s2);    // S { v: [0, 1, 2, 3, 4], w: 10.75 }
    println!("{:?}, {:?}", &s, &s2); // S { v: [0, 1, 2], w: 0.5 }, S { v: [3, 4], w: 10.25 }
    s += &s2;
    println!("{:?}", s);    // S { v: [0, 1, 2, 3, 4], w: 10.75 }
    println!("{}", &s==&s);  // true
    println!("{}", &s!=&s);  // false
    println!("{}", &s==&s2); // false
    let s3 = S {v: vec![0, 1, 2], w: std::f64::NAN};
    println!("{}", s3==s3); // false
    println!("{:?}", s==s2); // false
    println!("{:?}", s>s2); // true
    println!("{:?}", s<s2); // false
    println!("{}", s[0]); // 0
    s[0] = 1;
    println!("{}", s[0]); // 1
}

今回はここまで。
Rust勉強中シリーズにタイトル付けました。自分でもどれがなんやったっけってなってしまったので笑

11
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8