演算子オーバーロード
この章で扱うトピックは、ほとんどクラスにも適用される。最大の違いは、クラスでは代入演算子opAssign()
の動作をオーバーロードできないことだ。
演算子オーバーロードには多くの概念が含まれており、その一部は本書の後半で説明する(テンプレート、auto ref
など)。そのため、この章は前の章よりも理解が難しいかもしれない。
演算子オーバーロードを使用すると、演算子とともに使用されたときに、ユーザー定義型がどのように動作するかを定義することができる。この文脈では、オーバーロードとは、特定の型に対する演算子の定義を提供することを意味する。
前の章では、構造体とそのメンバー関数の定義方法について説明した。例として、TimeOfDay
オブジェクトにDuration
オブジェクトを追加できるように、メンバー関数increment()
を定義した。前の章で説明した2つの構造体を、この章に関連する部分だけ抜粋して以下に示す。
メンバー関数の利点は、その型のメンバー変数とともに、その型の操作を定義できることだ。
その利点にもかかわらず、メンバー関数は、基本型に対する操作に比べて制限があるといえるだろう。結局のところ、基本型は演算子で簡単に使用できる。
これまで見てきたように、ユーザー定義型では、同様の操作はメンバー関数でしか実現できない。
演算子オーバーロードを使用すると、構造体やクラスも演算子として使用できるようになる。例えば、TimeOfDay
に対して+=
演算子が定義されている場合、上記の操作は基本型の場合とまったく同じように記述できる。
演算子オーバーロードの詳細に入る前に、まず、上記の行がTimeOfDay
でどのように有効になるかを確認しよう。必要なのは、increment()
メンバー関数を特別な名前opOpAssign(string op)
で再定義し、この定義が+
文字用であることを指定することだ。後で説明するように、この定義は実際には+=
演算子に対応している。
このメンバー関数の定義は、これまで見てきたものとは異なる。これは、opOpAssign
が実際には関数テンプレートであるためだからだ。テンプレートについては、後の章で詳しく説明するので、ここでは演算子オーバーロードの構文をそのまま受け入れる。
テンプレートの定義は2つの部分から成る:
opOpAssign(string op)
: この部分はそのまま記述し、関数の名前として受け入れる必要がある。opOpAssign
以外のメンバー関数があることは、後で説明する。if (op ==
:"+"
)opOpAssign
は、複数の演算子オーバーロードに使用される。
は、これが"+"
+
文字に対応する演算子オーバーロードであることを指定する。この構文はテンプレート制約であり、後の章で説明する。
また、今回は、戻り値の型がincrement()
メンバー関数の戻り値の型とは異なっていることにも注意。これは、もはやvoid
ではない。演算子の戻り値の型については、後で説明する。
内部では、コンパイラは+=
演算子の使用を、opOpAssign!
関数呼び出しに置き換える。"+"
The !
"+"
opOpAssign
の後の部分は、この呼び出しが+
文字のオペレーターの定義であることを指定している。このテンプレート構文については、後続の章でも説明する。
+=
に対応する演算子の定義は、 "+"
によって定義され、 "+="
によって定義されることに注意。opOpAssign()
の名前にあるAssign
は、この名前が代入演算子であることを既に示している。
演算子の動作を定義できるということは、責任も伴う。プログラマは期待される動作を遵守しなければならない。極端な例としては、前の演算子を、時間値を増加させるのではなく減少させるように定義することも可能だった。しかし、コードを読む人は、+=
演算子によって値が増加すると期待するだろう。
ある程度、演算子の戻り値の型も自由に選択できる。ただし、戻り値の型についても、一般的な期待は守らなければならない。
不自然な動作をする演算子は混乱やバグを引き起こす可能性があることを念頭に置いてほしい。
オーバーロード可能な演算子
オーバーロードできる演算子にはさまざまな種類がある。
一項演算子
単一のオペランドを取る演算子を単項演算子と呼ぶ:
++
は一元演算子である。なぜなら、単一の変数に対して動作するからだ。
単項演算子は、opUnary
という名前のメンバー関数によって定義される。opUnary
は、演算子が実行されているオブジェクトのみを使用するため、引数は取らない。
オーバーロード可能な一項演算子と、それに対応する演算子文字列は次の通りだ。
演算子 | 説明 | 演算子文字列 |
---|---|---|
-object | 負の値(数値の補数) | "-" |
+object | と同じ値(またはそのコピー) | "+" |
~object | ビット単位の否定 | "~" |
*object | 指す先へのアクセス | "*" |
++object | 加算 | "++" |
--object | 減算 | "--" |
例えば、Duration
の演算子++
は次のように定義できる。
ここで、演算子の戻り値の型もref
と指定されていることに注意しよう。これについては、後で説明する。
Duration
オブジェクトは現在、++
で加算できる:
後増分演算子および後減算演算子はオーバーロードできない。object++
およびobject--
の使用は、オブジェクトの前の値を保存することでコンパイラによって自動的に処理される。例えば、コンパイラは後増分に対して、次のコードと同等の処理を適用する。
他の言語とは異なり、Dでは、後増分演算子の式が実際に使用されない場合、後増分演算子内のコピーにはコストは発生しない。これは、コンパイラが、このような後増分演算子を、それに対応する前増分演算子に置き換えるためだ。
上記では、i
の前の値は実際には使用されていないため、コンパイラは式を次の式に置き換える。
さらに、opBinary
のオーバーロードがduration += 1
の使用をサポートしている場合、opUnary
は++duration
およびduration++
のためにオーバーロードする必要はない。その代わりに、コンパイラはバックグラウンドでduration += 1
式を使用する。同様に、duration -= 1
のオーバーロードは、--duration
およびduration--
の使用もカバーする。
二項演算子
2つのオペランドを取る演算子を二項演算子と呼ぶ:
上記の行には2つの別々の二項演算子がある。2つのオペランドの値を足し合わせる+
演算子と、右側のオペランドの値を左側のオペランドに代入する=
演算子だ。
下の右端の列は、各演算子のカテゴリを説明している。"="でマークされたものは、左側のオブジェクトに代入する。
演算子 | 説明 | 関数名 | 右辺の関数名 | カテゴリ |
---|---|---|---|---|
+ | 加算 | opBinary | opBinaryRight | 算術 |
- | 減算 | opBinary | opBinaryRight | 算術 |
* | 乗算 | opBinary | opBinaryRight | 算術 |
/ | 除算 | opBinary | opBinaryRight | 算術 |
% | 余り | opBinary | opBinaryRight | 算術 |
^^ | 累乗 | opBinary | opBinaryRight | 算術 |
& | ビット単位の | opBinary | opBinaryRight | ビット単位 |
| | ビット単位のOR | opBinary | opBinaryRight | ビット単位 |
^ | ビット単位の排他的論理和 | opBinary | opBinaryRight | ビット単位 |
<< | 左シフト | opBinary | opBinaryRight | ビット単位 |
>> | 右シフト | opBinary | opBinaryRight | ビット単位 |
>>> | 符号なし右シフト | opBinary | opBinaryRight | ビット単位 |
~ | 連結 | opBinary | opBinaryRight | |
in | 含まれているかどうか | opBinary | opBinaryRight | |
== | 等しいかどうか | opEquals | - | 論理 |
!= | 等しくないかどうか | opEquals | - | 論理 |
< | それより前かどうか | opCmp | - | ソート |
<= | 後かどうか | opCmp | - | ソート |
> | 後 | opCmp | - | ソート |
>= | 並べ替え前 | opCmp | - | ソート |
= | 割り当て | opAssign | - | = |
+= | 加算 | opOpAssign | - | = |
-= | 減算 | opOpAssign | - | = |
*= | 乗算と割り当て | opOpAssign | - | = |
/= | 除算して代入 | opOpAssign | - | = |
%= | の余りを代入 | opOpAssign | - | = |
^^= | 累乗の結果を代入 | opOpAssign | - | = |
&= | &の結果を代入 | opOpAssign | - | = |
|= | |の結果を代入する | opOpAssign | - | = |
^= | ^の結果を割り当てる | opOpAssign | - | = |
<<= | 結果を<<に割り当てる | opOpAssign | - | = |
>>= | >>の結果を代入する | opOpAssign | - | = |
>>>= | >>>の結果を代入する | opOpAssign | - | = |
~= | 追記 | opOpAssign | - | = |
opBinaryRight
は、オブジェクトが演算子の右側に現れることができる場合に使用する。プログラムにopという名前の二項演算子が現れると仮定しよう:
呼び出すメンバー関数を決定するために、コンパイラは次の2つの選択肢を検討する。
コンパイラは、もう一方よりも一致するオプションを選択する。
opBinaryRight
は、通常、int
のように演算子の両側で機能する算術型を定義する場合に有用だ。
opBinaryRight
のもう1つの一般的な用途は、in
演算子だ。通常、in
の右側に現れるオブジェクトに対して、opBinaryRight
を定義するほうが理にかなっている。この例を以下に示す。
以下の定義で登場するパラメータ名rhs
は、右辺の略語だ。これは、演算子の右側に現れるオペランドを表している。
上記の式では、rhs
パラメータは変数y
を表す。
要素インデックスとスライシング演算子
次の演算子を使用すると、型を要素のコレクションとして使用することができる。
説明 | 関数名 | 使用例 |
---|---|---|
要素へのアクセス | opIndex | collection[i] |
要素への代入 | opIndexAssign | collection[i] = 7 |
要素に対する一項演算 | opIndexUnary | ++collection[i] |
要素に対する代入を伴う演算 | opIndexOpAssign | collection[i] *= 2 |
要素の数 | opDollar | collection[$ - 1] |
すべての要素のスライス | opSlice | collection[] |
一部の要素のスライス | opSlice(size_t, size_t) | collection[i..j] |
これらの演算子は後で説明する。
以下の演算子関数は、以前のバージョンのDのものだ。これらの使用は推奨されない。
説明 | 関数名 | 使用例 |
---|---|---|
すべての要素に対する一項演算 | opSliceUnary (discouraged) | ++collection[] |
一部の要素に対する一項演算 | opSliceUnary (discouraged) | ++collection[i..j] |
すべての要素への代入 | opSliceAssign (discouraged) | collection[] = 42 |
一部の要素への代入 | opSliceAssign (discouraged) | collection[i..j] = 7 |
すべての要素に対する代入を含む演算 | opSliceOpAssign (discouraged) | collection[] *= 2 |
一部の要素に対する代入を含む演算 | opSliceOpAssign (discouraged) | collection[i..j] *= 2 |
その他の演算子
以下の演算子もオーバーロードすることができる。
説明 | 関数名 | 使用例 |
---|---|---|
関数呼び出し | opCall | object(42) |
型変換 | opCast | to!int(object) |
存在しない関数のディスパッチ | opDispatch | object.nonExistent() |
これらの演算子は、以下の各セクションで説明する。
複数の演算子を同時に定義する
コード例を短くするため、ここでは++
、+
、+=
の3つの演算子のみを使用している。ある型に対して1つの演算子がオーバーロードされる場合、他の多くの演算子もオーバーロードする必要があることが考えられる。例えば、--
および-=
演算子は、次のDuration
上記の演算子オーバーロードにはコードの重複がある。類似の関数の違いのみを強調表示している。このようなコードの重複は、文字列ミックスインを使用することで削減でき、場合によっては完全に回避することもできる。mixin
キーワードについては、後の章でも説明する。このキーワードが演算子オーバーロードにどのように役立つかを簡単に紹介したいと思う。
mixin
は、mixin
文がコード内に現れる場所に、指定した文字列をソースコードとして挿入する。次の構造体は、上記の構造体と同じ意味だ。
Duration
オブジェクトが量で乗算や除算される必要がある場合、テンプレート制約に2つの条件を追加するだけで済む:
実際、テンプレート制約はオプションだ:
演算子の戻り値の型
演算子をオーバーロードする場合、基本型に対する同じ演算子の戻り値の型を遵守することをお勧めする。これにより、コードの意味が理解しやすくなり、混乱が減少する。
基本型に対する演算子は、void
を返すものはない。この事実は、一部の演算子では明らかだ。例えば、2つのint
値をa + b
として加算した結果は、int
になる。
他の演算子の戻り値はそれほど明白ではない場合もある。例えば、++i
のような演算子にも値がある。
++
演算子は、i
を加算するだけでなく、i
の新しい値も生成する。さらに、++
によって生成される値は、i
の新しい値ではなく、変数i
自体だ。この事実は、その式の結果のアドレスを出力することで確認できる。
出力には同じアドレスが含まれている:
iのアドレス | 7FFF39BFEE78 |
---|---|
++iの結果のアドレス | 7FFF39BFEE78 |
独自の型に対して演算子をオーバーロードする場合は、次のガイドラインに従うことをお勧めする。
- オブジェクトを修正する演算子
opAssign
を除き、オブジェクトを修正する演算子はオブジェクト自体を返すことが推奨される。このガイドラインは、上記のTimeOfDay.opOpAssign!
および"+"
Duration.opUnary!
で遵守されている。"++"
次の2つの手順で、オブジェクト自体を返すことができる:
- 戻り値の型は構造体の型で、参照を表すキーワード
ref
でマークされている。 return this
は、このオブジェクトを返すことを意味して、関数を終了する。
オブジェクトを修正する演算子は
opUnary!
,"++"
opUnary!
, およびすべての"--"
opOpAssign
のオーバーロードである。 - 戻り値の型は構造体の型で、参照を表すキーワード
- 論理演算子
opEquals
==
と!=
の両方を表す論理演算子は、bool
を返す必要がある。in
演算子は通常、含まれるオブジェクトを返すが、bool
を返すこともできる。 - ソート演算子
opCmp
<
、<=
、>
、および>=
を表すものは、int
を返す必要がある。 - 新しいオブジェクトを作成する演算子
一部の演算子は、新しいオブジェクトを作成して返す必要がある:
- 一項演算子
-
、+
、~
、および二項演算子~
。 - 算術演算子
+
、-
、*
、/
、%
、および^^
。 - ビット演算子
&
、|
、^
、<<
、>>
、および>>>
。 - 前の章で見たように、
opAssign
は、return this
によってこのオブジェクトのコピーを返す。注釈:最適化のため、大きな構造体では、
opAssign
がconst ref
を返すほうが理にかなっている場合がある。この本では、この最適化は適用しない。
新しいオブジェクトを作成する演算子の例として、
opBinary!
"+"
Duration
のオーバーロードを定義しよう。この演算子は、2つのDuration
オブジェクトを加算して新しいオブジェクトを作成し、それを返す。この定義により、
+
オペレーターを使用してDuration
オブジェクトを追加できるようになる:コンパイラは、その式を
travelDuration
オブジェクトの次のメンバー関数呼び出しに置き換える。 - 一項演算子
opDollar
コンテナの要素数を返すため、
opDollar
に最も適した型はsize_t
だ。ただし、戻り値の型は他の型(int
など)でもかまわない。- 制約のない演算子
一部の演算子の戻り値の型は、ユーザー定義型の設計に完全に依存する。単項演算子
*
、opCall
、opCast
、opDispatch
、opSlice
、およびすべてのopIndex
のバリエーション。
opEquals()
等価比較用
このメンバー関数は、==
および!=
演算子の動作を定義する。
opEquals
の戻り値の型はbool
である。
構造体では、opEquals
のパラメータはin
として定義できる。ただし、速度効率のため、opEquals
はauto ref const
を引数とするテンプレートとして定義できる (以下の空のテンプレート括弧にも注意)。
l値とr値の章で見たように、auto ref
により、l値は参照で、r値はコピーで渡すことができる。ただし、r値はコピーではなく移動されるため、上記のシグネチャはl値とr値の両方に効率的だ。
混乱を避けるため、opEquals
とopCmp
は一貫して動作する必要がある。opEquals
がtrue
を返す2つのオブジェクトに対して、opCmp
は0を返す必要がある。
opEquals()
が等価性のために定義されると、コンパイラはその逆を不等価性のために使用する。
通常、構造体に対してopEquals()
を定義する必要はない。コンパイラは、構造体に対してこれを自動的に生成する。自動的に生成されるopEquals
は、すべてのメンバーを個別に比較する。
2つのオブジェクトの等価性を、この自動動作とは異なって定義しなければならない場合がある。例えば、一部のメンバーがこの比較では重要ではない場合や、等価性がより複雑なロジックに依存する場合などだ。
例として、分単位の情報を完全に無視するopEquals()
を定義しよう。
等価比較ではhour
メンバーの値のみが考慮されるため、20:10と20:59は等価とみなされる。(これは単なる例であり、このような等価比較では混乱が生じることは明らかだ。)
opCmp()
並べ替えについて
ソート演算子は、オブジェクトのソート順を決定する。すべての順序演算子<
、<=
、>
、および>=
は、opCmp()
メンバー関数でカバーされている。
構造体については、opCmp
のパラメータをin
と定義することができる。ただし、opEquals
と同様に、opCmp
をauto ref const
を引数とするテンプレートとして定義するほうが効率的だ。
混乱を避けるため、opEquals
とopCmp
は一貫して動作する必要がある。opEquals
がtrue
を返す2つのオブジェクトに対して、opCmp
は0を返す必要がある。
次のコードのように、これらの4つの演算子のいずれかが使用されていると仮定しよう。
コンパイラは、その式を次の論理式に変換し、新しい論理式の結果を使用する。
<=
演算子について考えてみよう:
コンパイラは、次のようにコードを生成する:
ユーザー定義のopCmp()
が正しく動作するには、このメンバー関数は次の規則に従って結果を返さなければならない。
- 左側のオブジェクトが右側のオブジェクトよりも前であるとみなされる場合は、負の値
- 左側のオブジェクトが右側のオブジェクトよりも後であるとみなされる場合は、正の値
- 両オブジェクトが同じソート順であるとみなされる場合、0
これらの値をサポートするには、opCmp()
の戻り値の型は、bool
ではなく、int
である必要がある。
以下は、hour
メンバーの値を最初に比較し、次にminute
メンバーの値を比較して (hour
メンバーが等しい場合のみ)、TimeOfDay
オブジェクトを順序付ける方法である。
この定義は、hour
メンバーが等しい場合はminute
値の差を、そうでない場合はhour
メンバーの差を返す。戻り値は、左側のオブジェクトが時系列で先の場合に負の値、右側のオブジェクトが先の場合に正の値、両者がまったく同じ時刻を表す場合は0になる。
警告:メンバーの有効な値がオーバーフローを引き起こす可能性がある場合、opCmp
の実装に減算を使用することはバグである。例えば、以下の2つのオブジェクトは、値-2
のオブジェクトが値int.max
のオブジェクトよりも大きいと計算されるため、正しくソートされない。
一方、TimeOfDay
では、そのstruct
のメンバーの有効な値はいずれも減算でオーバーフローを起こさないため、減算を使用しても問題はない。
std.algorithm.cmp
は、スライス(すべての文字列型および範囲を含む)の比較に使用できる。cmp()
は、スライスを辞書順で比較し、その順序に応じて負の値、0、または正の値を生成する。この結果は、opCmp
の戻り値として直接使用できる。
opCmp()
が定義されると、この型はstd.algorithm.sort
などのソートアルゴリズムでも使用できるようになる。sort()
は要素に対して動作するため、その順序を決定するために、バックグラウンドでopCmp()
演算子が呼び出される。次のプログラムは、ランダムな値を持つ10個のオブジェクトを作成し、sort()
でそれらをソートする。
予想通り、要素は最も早い時間から最も遅い時間順に並べ替えられている:
[03:40, 04:10, 09:06, 10:03, 10:09, 11:04, 13:42, 16:40, 18:03, 21:08]
opCall()
オブジェクトを関数として呼び出す
関数を呼び出すときのパラメータリストを囲む括弧も演算子だ。static opCall()
によって、型の名前を関数として使用できることはすでに説明した。static opCall()
を使用すると、実行時にデフォルト値を持つオブジェクトを作成できる。
一方、非静的opCall()
を使用すると、ユーザー定義型のオブジェクトを関数として使用することができる。
上記のオブジェクトfoo
は、関数のように呼び出されている。
例として、線形方程式を表すstruct
を考えてみよう。このstruct
は、特定のx値について、次の線形方程式のy値を計算するために使用される。
y = ax + b
次のopCall()
は、その方程式に従ってyの値を計算して返すだけだ。
この定義により、LinearEquation
の各オブジェクトは、特定のaおよびbの値に対する線形方程式を表す。このようなオブジェクトは、yの値を計算する関数として使用できる。
注釈: struct
にopCall()
を定義すると、コンパイラによって生成される自動コンストラクタが無効になる。そのため、上記では、推奨されるLinearEquation(1.2, 3.4)
ではなく、{ }
という構文を使用している。後者の構文を使用する場合は、2つのdouble
パラメータを取るstatic opCall()
も定義する必要がある。
equation
は、y = 1.2x + 3.4という線形方程式を表している。このオブジェクトを関数として使用すると、opCall()
メンバー関数が実行される。
この機能は、aおよびbの値を1回だけオブジェクトで定義して保存し、そのオブジェクトを後で複数回使用する場合に便利だ。次のコードは、このようなオブジェクトをループで使用している。
このオブジェクトは、y=0.01x+0.4の式を表している。これは、0.0から1.0までの範囲のx値の結果を計算するために使用されている。
インデックス演算子
opIndex
、opIndexAssign
、opIndexUnary
、opIndexOpAssign
、およびopDollar
を使用すると、object[index]
のように、配列と同様にユーザー定義型にインデックス演算子を使用することができる。
配列とは異なり、これらの演算子は多次元インデックスもサポートしている。複数のインデックス値は、角括弧で囲んだカンマ区切りのリストとして指定する(例:object[index0, index1]
)。以下の例では、これらの演算子を1次元の場合にのみ使用し、多次元での使用法はテンプレートの詳細の章で説明する。
以下の例で、deque
変数は、以下で定義するstruct DoubleEndedQueue
のオブジェクトであり、e
はint
型の変数だ。
opIndex
は要素へのアクセス用である。括弧内に指定したインデックスが演算子関数のパラメータになる。
opIndexAssign
は要素への代入用だ。最初のパラメータは代入される値で、2番目のパラメータは要素のインデックスだ。
opIndexUnary
opUnary
と類似している。違いは、指定されたインデックスの要素に操作が適用される点だ:
opIndexOpAssign
opOpAssign
と同様だ。違いは、操作が要素に適用される点である:
opDollar
インデックス付けとスライシング時に使用される$
文字を定義する。コンテナ内の要素数を返すためのものだ:
インデックス演算子の例
両端キューは配列に似たデータ構造だが、コレクションの先頭に効率的に挿入することもできる。(対照的に、配列の先頭に挿入する操作は、既存の要素を新しく作成した配列に移動する必要があるため、比較的遅い操作である。)
ダブルエンドキューを実装する1つの方法は、背景に2つの配列を使用し、最初の配列を逆順で使用することだ。キューの先頭に概念的に挿入される要素は、実際には先頭配列の末尾に追加される。その結果、この操作は末尾への追加と同じくらい効率的だ。
次のstruct
は、このセクションで見た演算子をオーバーロードする両端キューを実装している。
上記のガイドラインに従って、opOpAssign
の戻り値の型は、~=
演算子を同じコレクションで連鎖できるように、ref
になる。
その結果、100と200は同じコレクションに追加される:
インデックス3の要素 | 3 |
---|
9 | 7 | 5 | 3 | 2 | 55 | 68 | 4 | 6 | 8 | 100 | 200 |
スライシング演算子
opSlice
[]
演算子を使用して、ユーザー定義型のオブジェクトをスライスすることができる。
この演算子の他に、opSliceUnary
、opSliceAssign
、opSliceOpAssign
もあるが、これらは使用しないように。
Dは多次元スライスをサポートしている。多次元の例については、テンプレートの詳細の章で後で説明する。その章で説明する方法は 1次元にも使用できるが、上記で定義したインデックス演算子と一致せず、まだ説明していないテンプレートを使用するため、ここでは説明しない。そのため、この章では、単一次元でのみ機能するテンプレートを使用しないopSlice
の使い方を説明する。(このopSlice
の使い方も推奨されない。)
opSlice
には2つの異なる形式がある:
- 角括弧は空にすることもでき、
deque[]
のようにすべての要素を意味する。 deque[begin..end]
角括弧内に数値範囲を指定すると、指定された範囲内の要素を意味する。
スライシング演算子は、他の演算子よりも比較的複雑である。これは、コンテナと範囲という2つの異なる概念を扱うためだからだ。これらの概念については、後続の章で詳細に説明する。
テンプレートを使用しない 1次元スライシングでは、opSlice
は、コンテナの特定の範囲の要素を表すオブジェクトを返す。opSlice
が返すオブジェクトは、その範囲の要素に適用される操作を定義する役割を担う。例えば、裏では、まずopSlice
を呼び出して範囲オブジェクトを取得し、次にそのオブジェクトに opOpAssign!
を適用する。"*"
したがって、DoubleEndedQueue
のopSlice
演算子は、操作が適用される特別なRange
オブジェクトを返す。
出力:
9 | 7 | 5 | 3 | 1 | 0 | 2 | 4 | 6 | 8 |
90 | 70 | 50 | -1 | -1 | -1 | -1 | 40 | 60 | 80 |
opCast
型変換
opCast
は、明示的な型変換を定義する。これは、各ターゲット型に対して個別にオーバーロードすることができる。前の章で覚えているように、明示的な型変換は、to
関数とcast
演算子によって実行される。
opCast
もテンプレートだが、形式が異なる。ターゲット型は、(T : target_type)
構文で指定する。
この構文は、テンプレートの章で後ほど説明される。
Duration
の定義を変更して、hoursとminutesの2つのメンバーを持つようにしよう。この型のオブジェクトをdouble
に変換する演算子は、次のコードのように定義できる。
コンパイラは、上記の型変換呼び出しを次のように置き換える。
上記のdouble
変換関数は、2時間30分に対して2.5を返す。
2.5
opCast
は明示的な型変換用だが、変数が論理式で使用されると、そのbool
特殊化が自動的に呼び出される。
ただし、opCast
のbool
特殊化は、すべての暗黙のbool
変換に対応しているわけではない:
キャッチオール演算子opDispatch
opDispatch
は、オブジェクトの欠落しているメンバーにアクセスするたびに呼び出される。存在しないメンバーにアクセスしようとした場合は、すべてこの関数にディスパッチされる。
欠落しているメンバーの名前は、opDispatch
のテンプレートパラメータの値になる。
以下のコードは、簡単な定義を示している:
存在しないメンバーを呼び出しても、コンパイラエラーは発生しない。代わりに、これらの呼び出しはすべてopDispatch
にディスパッチされる。最初のテンプレートパラメータは、メンバーの名前である。関数を呼び出すときに使用されるパラメータ値は、opDispatch
のパラメータとして表示される。
名前 | 値 |
---|---|
aNonExistentFunction | 42 |
anotherNonExistentFunction | 100 |
name
テンプレートパラメータは、その関数内で、その特定の存在しない関数の呼び出しをどのように処理するかを決定するために使用できる。
包含クエリによるopBinaryRight!"in"
この演算子を使用すると、ユーザー定義型に対するin
演算子の動作を定義することができる。in
は、通常、連想配列で、特定のキーの値が配列に存在するかどうかを判断するために使用される。
他の演算子とは異なり、この演算子は通常、オブジェクトが右側に現れる場合にオーバーロードされる。
コンパイラは内部でopBinaryRight
を使用する:
特定のキーの値が配列に存在しないかどうかを判断するための!in
もある。
!in
はオーバーロードできない。これは、コンパイラが代わりにin
演算子の結果の否定を使用するためだ。
in
演算子の例
次のプログラムでは、Duration
およびTimeOfDay
に加えて、TimeSpan
型を定義している。TimeSpan
で定義されているin
演算子は、ある時点がその時間範囲内にあるかどうかを判断する。
コードを短くするため、次のプログラムでは必要なメンバー関数のみを定義している。
TimeOfDay
オブジェクトが、for
ループでシームレスに使用されていることに注目。このループは、演算子オーバーロードの有用性を示す例である。
出力:
11:30は昼休み外
11:45は昼休み外
12:00は昼休み中
12:15は昼休み中
12:30は昼休み中
12:45は昼休み中
13:00は昼休み外
13:15は昼休み外
演習
分子と分母をlong
型のメンバーとして格納する分数型を定義しよう。この型は、float
、double
、real
のような精度による値の損失がないため、便利だ。例えば、double
型の値1.0/3に3を掛けた結果は1.0ではないが、分数1/3を表すFraction
オブジェクトに3を掛けると、結果は正確に1になる。
この型を、できるだけ基本型に近い便利な型にするために、必要に応じて演算子を定義しよう。型の定義が、以下のユニットテストをすべて合格することを確認しよう。ユニットテストは、以下の動作を保証する。
- 分母が0のオブジェクトを構築すると、例外がスローされる必要がある。(これは、上記の
enforce
式で既に処理されている。) - 値の負の値を生成する。例えば、1/3の負の値は-1/3になり、-2/5の負の値は2/5になる。
++
および--
によって値を1増分および1減算する。- 4つの算術演算のサポート:
+=
、-=
、*=
、/=
によってオブジェクトの値を変更すること、および+
、-
、*
、/
演算子を使用して2つのオブジェクトを使用した結果を生成すること。(コンストラクタと同様に、0による除算は防止する必要がある。)思い出として、2つの分数a/bとc/dを含む算術演算の式を以下に示す。
- 加算: a/b + c/d = (a*d + c*b)/(b*d)
- 減算: a/b - c/d = (a*d - c*b)/(b*d)
- 乗算: a/b * c/d = (a*c)/(b*d)
- 除法: (a/b) / (c/d) = (a*d)/(b*c)
- オブジェクトの実際の(そして必然的に損失のある)値は、
double
に変換することができる。 - 並べ替え順や等価比較は、分子と分母の値ではなく、分数の実際の値によって行われる。例えば、分数1/3と20/60は等しいとみなされる。