03 式と演算子
Contents
- 式と演算子
- 演算子と暗黙の数値型変換
- 数値型に対する演算子
- 括弧(bracket)
- 単項プラス(unary plus), 単項マイナス(unary minus)
- インクリメント(increment), デクリメント(decrement)
- 算術演算子
- ビット演算子(Bitwise operators)
- 代入演算子(Assignment operator), 複合代入演算子(Compound assignment operators)
- 同値テスト(Equal to), 非同値テスト(Not equal to)
- 比較(Compare)
- 論理演算子(Logical operators)
- コンマ演算子
- 付録 演算子の優先順位と結合規則
- 問題 -> 解答
- おわりに
- キーワード
式と演算子
ここからは式のお話をします。
式は、D言語での計算の一番小さな単位です。
たとえば、1 + 2 + 3
は式ですが、この式に含まれる1 + 2
, 1
, 2
, 3
も同様に式です。
1
や2
, 3
などはリテラル(Literal)と呼ばれると前回説明しましたね。
式は演算子などによって、より大きな式を作ります。
式は評価(Evaluation)されると値を持ちます。
たとえば1
ならint
型の1
、int a = 12;
としてa + 2
の式を評価するとint
型の14
という値になります。
a + b
のa
とb
の評価は同時に行われるわけではなく、a
が先に評価され、次にb
が評価されます。
この評価順序は演算子によって異なりますが、2項演算子であれば左が優先されます。
ちなみに、この評価順序はそれほど重要ではありませんし、クリティカルな問題にもなりません。
2項演算子は左優先ということ以外、覚えている人や知っている人は私を含めてほぼ皆無であると思います。
(a[b]
という演算子は、b
を評価した後にa
を評価します。関数呼び出しのa(b)
は逆にa
を評価してb
を評価します。)
(要出典, コンパイラの実装によっては異なっても良い?)
また、演算子にはそれぞれ結合規則が定められています。
たとえば、1.0 / 2 / 5
は(1.0 / 2) / 5
となってdouble
型の2.5
と評価されます。
もし、1.0 / (2 / 5)
と解釈されるなら、2 / 5
は整数であるint
型なので、0
と評価され、結果的に1.0 / 0
となります。
これが、評価されるとdouble.inf
という値になります。
つまり、演算子の結合の方向が右なのか左なのかによって式の値は異なるので、演算子ごとに結合の向きを定義しておく必要があります。
最後に、演算子には優先度という順位があります。
先に紹介した割り算の例では、式中に現れる演算子の優先順位がすべて同じであったため、結合規則に則って評価されました。
では、1 + 4 / 2
という式があった場合にはどうすべきでしょうか?
掛け算や割り算は、足し算や引き算よりも先に計算することを小学校の算数で勉強したと思います。
D言語でも(大体のプログラミング言語でも)同じ規則が成り立ちます。
この場合には、1 + 4
よりも先に4 / 2
が評価されて、1 + 2
となり、最終的に3
と評価されます。
プログラマはこれらの評価の順序を覚えているかというと、正確に把握できている人は少ないと思います。
ですから、1 + 4 / 2
や1.0 / 2 / 5
などという明らかな場合はいいのですが、複雑な場合には、カッコ()
で式をくくりましょう。
数学と同じように、()
でくくればその中の優先順位は最も高くなります。
(1 + 4) / 2
なら曖昧性が一切ありません。
しかし、残念ながらソースコードでは、中括弧{}
や大括弧[]
は、このような目的で使えません。
演算子と暗黙の数値型変換
数値型はたくさんありますが、それ故に異なる型との演算がよくあります。
この項では、たとえばfloat + int
のような異なる数値型同士の演算の型について説明します。
数値型の演算子の規則は、簡単に言えば「大きい型や浮動小数点型に変換される可能性がある」です。
たとえば、float + int
はどちらもfloat + float
になってfloat
になります。
以下に様々な例を示します。
writeTypeはよくわからないと思いますが、2つの型で2項演算してみた結果の型を出力する関数です。
import std.stdio;
void writeType(T, string op, U)()
{
writeln(typeof(mixin("T.init " ~ op ~ " U.init")).stringof);
}
void main()
{
writeType!( byte, "+", byte)(); // int
writeType!( byte, "+", ubyte)(); // int
writeType!( ubyte, "+", ubyte)(); // int
writeln();
writeType!( short, "+", short)(); // int
writeType!( short, "+", ushort)(); // int
writeType!( ushort, "+", ushort)(); // int
writeln();
writeType!( int, "+", int)(); // int
writeType!( int, "+", uint)(); // uint
writeType!( uint, "+", uint)(); // uint
writeln();
writeType!( long, "+", long)(); // long
writeType!( long, "+", ulong)(); // ulong
writeType!( ulong, "+", ulong)(); // ulong
writeln();
writeType!( int, "+", long)(); // long
writeType!( int, "+", ulong)(); // ulong
writeType!( uint, "+", long)(); // long
writeType!( uint, "+", ulong)(); // ulong
writeln();
writeType!( float, "+", float)(); // float
writeType!( float, "+", ifloat)(); // cfloat
writeType!( ifloat, "+", ifloat)(); // ifloat
writeln();
writeType!( real, "+", float)(); // real
writeType!( real, "+", ifloat)(); // creal
writeType!( ireal, "+", ifloat)(); // ireal
writeln();
writeType!( float, "+", cfloat)(); // cfloat
writeType!( ifloat, "+", cfloat)(); // cfloat
writeType!( cfloat, "+", cfloat)(); // cfloat
writeln();
writeType!( ifloat, "*", ifloat)(); // float
writeln();
writeType!( long, "+", float)(); // float
writeType!( long, "+", ifloat)(); // cfloat
}
例をよく見ると、int
より小さな整数型では、全てint
型になっています。
int
型以上の大きさの整数型では、より大きな型になります。
もう少しint
やlong
を詳しく見ると、頭に"u"
がついた符号なし整数の方が強いことがわかります。
浮動小数点型では、実数型と実数型の和はもちろん実数型、実数型と虚数型の和は複素数型、虚数型同士の和は虚数型というように理に適っています。 また、複素数型に実数型や虚数型, 複素数型を足しても結果は複素数型です。 しかし、虚数型同士の積は実数型となり、数学で習ったことと一致すると思います。
整数型と浮動小数点型では、浮動小数点型のほうが強く、浮動小数点型になります。
以上のことを踏まえると、以下の様な規則に従っていることがわかります。
- 規則1: 数値型の暗黙変換のルール
- 整数型から、より大きいサイズの整数型へは暗黙変換可能
- 実数浮動小数点型から、任意の大きさの実数浮動小数点型に暗黙変換可能
- 虚数浮動小数点型から、任意の大きさの虚数浮動小数点型に暗黙変換可能
- 複素数浮動小数点型から、任意の大きさの複素数浮動小数点型に暗黙変換可能
- 任意の整数型は、任意の実数浮動小数点型に暗黙変換可能
- 規則2: 演算子について、以下の規則は番号の小さいものから適応される。
どちらか片方が複素数型(浮動小数点型)である。
そのうち最大の型にどちらの項も暗黙変換され、結果はその型となる。どちらも虚数型(浮動小数点型)であり、積, 商の演算子である。
そのうち最大の型にどちらの項も暗黙変換され、結果はその大きさの実数型となる。どちらも虚数型(浮動小数点型)であり、和, 差の演算子である。
そのうち最大の型にどちらの項も暗黙変換され、結果はその大きさの虚数型となる。どちらか片方が虚数型(浮動小数点型)である。
そのうち最大の型にどちらの項も暗黙変換され、結果はその大きさの複素型となる。どちらか片方が浮動小数点型である。
そのうち最大の型にどちらの項も暗黙変換され、結果はその大きさの浮動小数点型となる。どちらか片方が
ulong
型である。
もう片方もulong
型に暗黙変換され、結果もulong
型。どちらか片方が
long
型である。
もう片方もlong
型に暗黙変換され、結果もlong
型。どちらか片方が
uint
型である。
もう片方もuint
型に暗黙変換され、結果もuint
型。その他の整数同士の演算
両方ともint
型に暗黙変換され、結果もint
型。
規則2はややこしいかもしれませんが、複素数や虚数が式に現れなければ、5~9のみを意識すればよく、それらの基礎は規則1に従っているので理解しやすいと思います。
数値型に対する演算子
D言語の数値型には、大きく分けると整数型と浮動小数点型があることを説明しました。 ここでは、その数値型で使える演算子について説明して行きたいと思います。 説明で特に記述がない限り、全ての数値型で使用可能です。 ただし、虚数型や複素型については、対応していないものが多くあります。
括弧(bracket)
int a = (1 + 2) / 1;
double d = (1.0 * 3) / 4.5;
カッコは最も優先される演算子で、優先順位は1となっています。
単項プラス(unary plus), 単項マイナス(unary minus)
int a = 12;
writeln(+a); // 12
writeln(-a); // -12
double d = 12;
writeln(+d); // 12
writeln(-d); // -12
cdouble c = 2 + 2i;
writeln(+c); // 2+2i
writeln(-c); // -2+-2i
単項マイナス演算子は、符号を反転させるときに使用します。 単項プラス演算子は、ソースコードを見やすくするためだけにあります。 この2つの演算子は、優先順位3となっています。
インクリメント(increment), デクリメント(decrement)
int a = 12;
writeln(++a); // 13 前置インクリメント
writeln(a); // 13
writeln(a++); // 13 後置インクリメント
writeln(a); // 14
writeln(--a); // 13 前置デクリメント
writeln(a); // 13
writeln(a--); // 13 後置デクリメント
writeln(a); // 12
インクリメントとは、値を一つ増やすことです。 デクリメントは逆に値を一つ減らします。 インクリメントもデクリメントも、左辺値(lvalue)でないと使えません。 理由は簡単で、そのように書いても加算された結果を格納できないからです。
インクリメントとデクリメントには、前置と後置があります。
前置の方を先に説明すると、「値を1
だけ{増やして/減らして}から、評価する」となります。
つまり、++a
は「a
の値を1
増やしてから、a
を評価する」ということです。
後置インクリメントや後置デクリメントは、「a
を評価した値を記憶しつつ、a
の値を1
だけ増やし、先ほど記憶した値を結果とする」という演算子です。
前置形式と後置形式で決定的に違うのがもう一つあります。 それは、「前置形式は左辺値を返すのに対して、後置形式は右辺値を返す」ことです。 インクリメントとデクリメントは左辺値に対して作用するので、後置形式の結果をインクリメントやデクリメントすることはできません。 逆に前置形式の結果はインクリメント,デクリメント可能です。
float a = 1;
writeln(++(++a)); // OK
writeln((++a)++); // OK
++a = 12; // OK
writeln((a++)++); // NG; Error: a++ is not an lvalue
writeln(++(a++)); // NG; Error: a++ is not an lvalue
a++ = 20; // NG; Error: a++ is not an lvalue
ちなみに、前置形式と後置形式では後置形式の方が優先順位が高くなっています。 前置形式は優先順位3であるのに対して、後置形式は2です。
cfloat c = 1 + 1i;
writeln(++c); // 2+1i
writeln(++c++); // NG; Error: c++ is not an lvalue
// ++(c++)と解釈されるため
writeln((++c)++); // OK
算術演算子
- 加算(Addition), 減算(Subtraction)
加算と減算は特に言うことは無いと思います。
加算の演算子+
と、減算の演算子-
の優先順位と結合規則は同じで、優先順位6の左→右への結合規則です。
2項演算子なので、左辺を先に評価します。
int a = 1,
b = 2;
writeln(a + b); // 3
writeln(a - b); // -1
int c = 3;
writeln(a + b + c); // (a + b) + c と解釈
writeln(a - b + c); // (a - b) + c と解釈
- 乗算(Multiplication), 除算(Division)
乗算や除算は優先順位が5と、加算や減算よりも優先されるようになっています。 結合規則は、左→右です。 また、この2つも2項演算子なので、左辺を先に評価します。
int a = 1,
b = 2;
writeln(a * b); // 2
writeln(a / b); // 0
int c = 3;
writeln(a * b * c); // (a * b) * c と解釈
writeln(a / b * c); // (a / b) * c と解釈
- 剰余(Modulo)
剰余演算子は、割り算での余りに相当します。 最後の例のように、浮動小数点数の演算では丸め誤差が存在するため、思った答えと違うものが出る可能性があります。 ほとんど整数でしか使用しないので気にはなりませんが、浮動小数点に対して使用する場合には注意が必要です。
優先順位は積や商と同じ5で、結合規則も同じ左→右です。
writeln(10 % 3); // 1; 10 / 3 = 3 .. 1
writeln(12 % 3); // 0; 12 / 3 = 3 .. 0
writeln(-10 % 3); // -1; -10 / 3 = -3 .. -1
writeln(-10 % -3); // -1; -10 / -3 = 3 .. -1
writeln( 10 % -3); // 1; 10 / -3 = -3 .. 1
writeln(11.6 % 3); // 2.6; 11.6 / 3 = 3 .. 2.6
writeln(11.6 % 3.5);// 1.1; 11.6 / 3.5 = 3 .. 1.1
writeln(3 % 0.6); // 0.6(誤差があるため)
- 累乗(Power)
プログラミングでは時々累乗を使うので、言語機能として累乗演算子が定義されています。
機能としては、std.math.pow
と同じです(std.math.pow
を呼んでるだけなので)。
優先順位は、もちろん積や商や剰余よりも一つ早い4となっています。
writeln(3 ^^ 3); // 27
writeln(0 ^^ 0); // 1
writeln(2.3 ^^ 4.4); // 39.0483
ビット演算子(Bitwise operators)
- 補数(Bitwise NOT)
int a = 0xFF00FF00;
writefln("%08X", a); // FF00FF00
writefln("%08X", ~a); // 00FF00FF
補数というのは、2進数では各ビットを反転させた結果に等しくなります。 補数は、組込みなどでしか使う機会は無いでしょうが、頭の片隅に置いておくと便利です。
もちろん、浮動小数点型には適用できません。
- AND, OR, XOR(Bitwize AND/OR/XOR)
writefln("%02X", 0xFF & 0xF0); // F0 AND
writefln("%02X", 0xFF | 0xF0); // FF OR
writefln("%02X", 0xFF ^ 0xF0); // 0F XOR
整数型のビット毎のAND, OR, XORを計算します。
プログラミングでは、よくマスクに利用されます。
たとえば、int
型の下位3bitの値が欲しければ、次のシフト演算子を使って(a & ((1 << 3) -1))
とします。
- 右シフト, 左シフト(Bitwise right/left shift operators)
int a = 4;
int b33 = 33;
writeln(a << 0); // 4
writeln(a << 1); // 8
writeln(a << 8); // 1024
writeln(a << b33); // 8
writeln(a >> 0); // 4
writeln(a >> 1); // 2
writeln(a >> 8); // 0
writeln(a >> b33); // 2
a = -15;
writeln(a); // -15
writeln(a >> 1); // -8
writeln(a >> 8); // -1
writeln(a >>> 1); // 2147483640
writeln(a >>> 8); // 16777215
a << b
は、a
のビット達をb
ビットだけ左にずらし、先頭を0で埋めます。
つまり、数値を2倍にできます。
a >> b
は、a
のビット達をb
ビットだけ右にずらし、aが自然数なら0, aが負の数なら1で先頭を埋めるため、数値を半分にできます。
a >>> b
は、a
のビット達をb
ビットだけ右にずらし、先頭を0で埋めます。
>>
とは違い、符号を考慮しません。
例では、a << 1
は2倍, a << 8
は2 ^^ 8 => 256
倍となっています。
ちなみに、int
やuint
型に対して32bit以上のシフトや、long
やulong
型に対して64bit以上のシフトをすると、次のような演算になります。
a >> b
=> a >> (b % (a.sizeof * 8))
a.sizeof * 8
の部分は、int
やuint
なら32, long
やulong
なら64となります。
この演算子も、浮動小数点型には適用できません。
代入演算子(Assignment operator), 複合代入演算子(Compound assignment operators)
int a;
writeln(a); // 0; 初期値
a = 12;
writeln(a); // 12
a += 12;
writeln(a); // 24
a -= 12;
writeln(a); // 12
a *= a;
writeln(a); // 144
a /= a / 4;
writeln(a); // 4
a %= 3;
writeln(a); // 1
++a ^^= 3;
writeln(a); // 8; (++a)が評価され2となり、a = 2 ^^ 3;と等価
代入演算子は、ビットのコピー、つまり数値をそのままコピーします。
複合代入演算子では、op=
の形を取り、op
は数値演算などの2項演算子が取れます。
a op= b
はa = a op b
と等価です(ただし、aの評価回数が1回減る)。
例にはありませんが、シフト演算子やビット演算子、配列の項で説明する結合演算子~
も可能です。
逆に、論理演算子は不可能です。
代入演算子と複合代入演算子は、左辺の値が左辺値でなければいけません。
同値テスト(Equal to), 非同値テスト(Not equal to)
int a = 1, b = 2;
writeln(a == a); // true
writeln(a == b); // false
writeln(a != a); // false
writeln(a != b); // true
数値型に対する同値テストは、両辺のビット表現が等しければtrue, そうでなければfalseとなります。
異なる型での比較は、型が格上げされてから比較されます。
非同値テストa != b
は、!(a == b)
に等しくなります。
浮動小数点型の場合には誤差があるため、同値テストは使いづらいでしょう。
std.math.approxEqual
を使えば誤差を含めて浮動小数点数が等しいかどうか比較できます。
詳しくは、std.math.approxEqual
を参照してください。
import std.math;
float a = 1;
float b = a * 1.0000001;
writeln(a == b); // false
writeln(approxEqual(a, b)); // true
比較(Compare)
int a = 1, b = 2;
writeln(a < a); // false
writeln(a <= a); // true
writeln(a > a); // false
writeln(a >= a); // true
writeln(a < b); // true
writeln(a <= b); // true
writeln(a > b); // false
writeln(a >= b); // false
数値型では、数学のように比較します。
<=
や>=
は、「≦」や「≧」に対応します。
複素数型については、比較は定義されていないので適用できません。
論理演算子(Logical operators)
- AND, OR, NOT
writeln(true && true); // true
writeln(true && false); // false
writeln(false && false); // false
writeln(true || true); // true
writeln(true || false); // true
writeln(false || false); // false
writeln(!true); // false
writeln(!!true); // true
writeln(0 || 1); // true
writeln(0 && 0); // false
writeln(!12); // false
writeln(!!12); // true
writeln(!0); // true
writeln(!!0); // false
論理演算子は、bool型の値のAND, OR, NOTを演算します。 もし、bool型以外の型の値を持つ式であれば、その式はbool型に評価されます(cast(bool)される)。
if文の項でも説明しますが、ここでも簡単に説明しておくと、数値型がbool
型に評価される場合には、0
がfalse
で、それ以外の場合はtrue
として評価されます。
さらに、&&
と||
の特殊性として、遅延評価が挙げられます。
a || b
は、a == true
ならば、b
を評価せずともtrue
であることがわかります。a && b
は、a == false
ならば、b
を評価せずともfalse
であることがわかります。
よって、a
がtrue
かfalse
によってb
が評価されるかどうかが異なります。
int a = 12;
a == 12 || writeln("foo"); // writeln("foo")は評価されない
a != 12 || writeln("bar"); // writeln("bar")は評価される
a == 12 && writeln("foo"); // writeln("foo")は評価される
a != 12 && writeln("bar"); // writeln("bar")は評価されない
- 条件演算子(Ternary conditional operator)
int a = 12;
ulong b = 13;
bool select = true;
writeln(select ? a : b); // 12
writeln(!select ? a : b); // 13
select ? writeln("foo") : writeln("bar"); // foo
(!select ? a : ++a) += 3;
writeln(a); // 16
コンマ演算子
面白い演算子としてコンマ演算子があります。 この演算子は複数の式を評価するものの、実際の式の値は最後の式の値になります。
writeln((12, 13, 14)); // 14
writeln((writeln("foo"), writeln("bar"), "hoge"));
// foo
// bar
// hoge
付録 演算子の優先順位と結合規則
今回出てきた演算子の優先順位と結合規則の一覧を以下に示しておきます。
ちなみに、全ての演算子の優先順位については次のページに定義されています。
Operator precedence - D Wiki
Operator Order of Evaluation Associativity: Example
(a) 1 →
a++ 2 ←
a-- 2 ←
a ^^ b 4 ←: 2 ^^ 3 ^^ 2 => 2 ^^ (3 ^^ 2)
++a 3 ←
--a 3 ←
+a 3 ←
-a 3 ←
!a 3 ←
~a 3 ←
a * b 5 →: 2 * 3 * 2 => (2 * 3) * 2
a / b 5 →: 2 * 3 / 2 => (2 * 3) / 2
a % b 5 →: 2 / 3 % 2 => (2 / 3) % 2
a + b 6 →
a - b 6 →
a << b 7 →
a >> b 7 →
a >>> b 7 →
a == b 8 →
a != b 8 →
a < b 8 →
a <= b 8 →
a > b 8 →
a >= b 8 →
a & b 9.0 →
a ^ b 9.1 →
a | b 9.2 →
a && b 10 →
a || b 11 →
a ? b : c 12 ←
a = b 13 ←: a = b = c => a = (b = c)
a += b 13 ←
a -= b 13 ←
a *= b 13 ←
a /= b 13 ←
a %= b 13 ←
a &= b 13 ←
a |= b 13 ←
a ^= b 13 ←
a <<= b 13 ←
a >>= b 13 ←
a >>>= b 13 ←
a, b 14 →
問題 -> 解答
シフト演算子の
>>
が、符号を維持できる理由は?
ヒント: 2の補数でGoogleで検索シフト演算子で
>>>
はあるのに<<<
がない理由は?
ヒント: 2の補数でGoogleで検索問題募集中
おわりに
お疲れ様です。 第三回「式と演算子」、長かったと思います。 前回も言いましたが、「基礎の基礎」が重要ですので、「早く何か書きたい!」と思ってても堪えてください。 基礎を固めることによって、相当プログラムの書ける範囲が広がりますし、他の言語へ移ってもすぐに適応できます。
さて、今回の問題は「調べ課題」みたいな感じですが、どうでしょうか? 計算機上で数値がどのように扱われているか知っておくと、思わぬバグに悩まされたときにより早く解決できるでしょう。
次回は、「標準入力」について説明したいと思います。
キーワード
- 評価(Evaluation)
- 暗黙の型変換(Implicit)
- 演算子(Operator)
- 演算子の優先順位(Priority of Operators, Order of Operators)
- 演算子の結合規則(Operator Associativity)