1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

代入と変換に関するよもやま話

Last updated at Posted at 2020-01-18

はじめに

Goコンパイラのインターフェース型変数への代入に関するソースコードを読んでいるうちに、「代入できるとはどういうことか(右辺値がどういう性格を持っているのか)」についていろいろと思いを巡らせることの方がおもしろくなってきたのでこの記事では雑多に「代入」および「代入するための変換」について書いていこうと思います。

Goのインターフェース

「Goコンパイラのソースを読んでGoを理解するぞ!」と始めた企画ですが止まっているうちにGoのバージョンが読んでいたものからだいぶ進んでしまいました。1

「全部読む」のではなく特に興味深いものについて、Goで一番おもしろいのってどれだろうと考えた結果、インターフェースがおもしろいなと思いました。
Goのインターフェースとは以下のように「インターフェースで定義したメソッドが、型のメソッドとして全部定義されていればインターフェース変数に代入して呼び出せる」というものです。Java等のように明示的に「実装する」という記述は必要ありません(そもそもクラス機能はありません2

インターフェースの例
package main

import "fmt"

type SampleInterface interface {
	Do()
	Show()
}

type SampleStruct1 struct {
	Value int
}

func (s *SampleStruct1) Do() {
	s.Value *= 2
}

func (s *SampleStruct1) Show() {
	fmt.Println(s.Value)
}

type SampleStruct2 struct {
	Value string
}

func (s *SampleStruct2) Do() {
	s.Value += s.Value
}

func (s *SampleStruct2) Show() {
	fmt.Println(s.Value)
}

func main() {
	var intf SampleInterface
	intf = &SampleStruct1{Value: 123}
	intf.Do()
	intf.Show()
	intf = &SampleStruct2{Value: "abc"}
	intf.Do()
	intf.Show()
}

「代入される型がインターフェースで定義されているメソッドを持っていると何故わかるのか」「別の構造体を代入できるということはインターフェースはポインタ的に実装されているのか」などが気になりコンパイラのソースを眺めました。
前者については予想通りに型チェックのところで「代入元(右辺)は代入先(左辺)のメソッドを持っているか」を確認しており、後者についてはSSA3への変換を流し読んだだけなのではっきりわかりませんが予想通りのことは行われていそうです。

ここまでは前説。以下が代入と変換に関するよもやま話です。

代入とは

まずは代入から始めましょう。多くのプログラミング言語では以下のようにして「値の代入」を行います。なおCの方が慣れているので以降のサンプルはC系の言語で書きます。

代入
int a;
a = 123;

C言語の場合、上記のコードは以下のようなアセンブリに変換されます。こちらも慣れてるのでx86/nasm系のアセンブリで書きます。なお実際にはローカル変数はスタックに置かれるとか話すと長くなるので雰囲気だけ見てください。

アセンブリに変換されるとこうなる
mov eax, 123

代入可能とは

単に変数の型を定義して、その型に合致する値を入れるだけではおもしろくもなんともありません。おもしろいのは「その型に入れることができない値」を代入しようとしたときです。例えば以下のコードを考えてみましょう。

これはOK?
int a;
a = 123.45;

「これはOK?」と書きましたがOKかNGかはプログラミング言語により異なります。
C(とC++)ではコンパイルができてaを表示すると123と表示されます。つまり、小数点以下が切り捨てられてaに代入されます4
JavaやC#ではコンパイルしようとすると「doubleからintには変換できない(大きい型を小さい型には入れられない)」のようなコンパイルエラーになります。キャストについては後ほど書きます。

C/C++はコンパイルができる(ただし問答無用で切り捨てる)、Java/C#はコンパイルエラーになるというのはコンパイラ設計者の思想や他の言語との関係などいろいろな要素が絡んでそうで楽しいですね。

話を型に移しましょう。doubleとintだと表現形式の話も出てきてめんどくさいので「byte型にint型の値を入れられるか」の話をします。なおC言語だと型の実際のバイトサイズがコンパイラ依存なのでここではJavaでの型サイズで話をします。

「変数は箱のようなもの」という説明は私もマサカリ投げたくなりますが、まあJavaの整数型について言えば確かに「箱」です。byte型の変数を格納するためには1バイト(8ビット)が必要です。

10進数123 = 2進数で01111011

int型の値は4バイト(32ビット)です。

10進数の12345678 = 2進数で00000111010110111100110100010101

8ビットしかない場所に「00000111010110111100110100010101」を入れようとしてもどう考えても入りません。このように、特にメモリに格納される形式に近い型の場合は「値が変数に入れられるか」は理解しやすいものになっています。

「型なし」言語での代入

Ruby、Python、JavaScriptなどの変数は宣言するときに型を指定しません。

Rubyでの代入
a = 123

そしてどのような「値」でも「代入」することができます。もちろん同じ変数にまったく意味の異なる値を入れるのは意味論的にNGですが。

数値も文字列も入れられるよ
a = 123.45
a = "abc"

これらの言語では「型のある言語」と変数の概念が異なります。そのため、今回の主題である代入可能と変換の話とは論点が異なるわけですが、これらの言語では「変数は箱のようなもの」という説明は完全に間違いで、「変数は名札」です。

スーパークラスとインターフェース

話を少し飛躍させて冒頭に書いたGoに近い話をします。ただしサンプルコードはGoではなくてJavaです。
オブジェクト指向な言語においてはスーパークラス型の変数にサブクラスのインスタンスを代入することができます。これは「サブクラスのインスタンスはスーパークラスのインスタンスでもある」からです。

インスタンス作成は実際にはどこかのメソッド内に書く必要があります
class Parent {}

class Child extends Parent {}

Parent p = new Child();

インターフェースについても同様に「インターフェースが実装されたクラスはインターフェース型の変数に代入」することができます。

変換

ここまででもいくつか出てきましたが、「そのままでは変数に代入できないもの」に対しては変換が行われます(上述のJavaのように変換が行われないでコンパイルエラーになる言語もあります)。この変換についても言語によりいろいろな特徴があっておもしろいです。

暗黙的な変換

値をそのままでは代入できない場合にしれっと変換してくれるものを「暗黙的な変換」と言います。上で示したC言語におけるint変数にdouble値を入れるような場合ですね。わかってて使うには便利ですが、わかっていないとバグの温床にもなる諸刃の剣です。

悪名高きPHP

暗黙的な変換、行き過ぎると余計なお世話の最たるものと言えばやはりPHPでしょう。なお悪名高きとは書いていますが私個人としてはPHPにそんなに恨みはないのであくまで暗黙的な変換のやり過ぎな例という意味です(笑)

PHPでは次の式の計算結果が15になります。文字列を数値に暗黙変換したうえで3を足しているわけですね。

「数値の文字列」と数値を足すと数値!
"12" + 3

PHP以外の言語で文字列と数値を足したときの挙動については大きく二つに分かれます。

  • 「足し算」が可能で"123"となる言語(JavaやJavaScript)
  • 「文字列と数値の足し算はできない」とエラーになる言語(RubyやPython。後述の「明示的な変換」が必要)

C++の1引数コンストラクタ

C++には1引数のコンストラクタに対するおもしろいルールがあります。以下のようにFoo f = 123;と書くとFoo f(123);と書いたことと同じになります。これも代入に対する暗黙の変換と言えます。なお、コンストラクタにexplicitを付けると代入でのオブジェクト作成はできなくなります。

代入でオブジェクトが作れる例
class Foo {
public:
    Foo(int data) {
        this->data = data;
    }

    void show() {
        cout << this->data << endl;
    }
    
private:
    int data;
};

int main(void){
    Foo f = 123; // Foo(int data)が呼ばれる
    f.show();
    return 0;
}

まあただ普通自分で暗黙変換1引数コンストラクタを使うことはなくて一番使うのはstringでしょう。

stringオブジェクトを代入で作る
int main(void){
    string s = "abc";
    cout << s << endl;
    return 0;
}

言語別「false」になるもの

if文を書く際に、「式の結果がbooleanじゃないとコンパイルエラーになる言語」と「booleanじゃなくてもいい言語」があります。後者の場合は「これはfalseとみなす。それ以外はtrue」と扱われます。これも暗黙的な変換の一種ですが言語によりfalseとなるものの扱いが異なる点に注意です。私が使うことのある言語で言うと以下のようになります。

C
0(NULLも多くの処理系では0)
Ruby
nil
Python
None, (), [], {}, '', 0
JavaScript
undefined, null, '', 0, NaN

C++のキャスト演算子オーバーロード

再びC++にご登場願いましょう。C++演算子オーバーロードというかっこいい機能があるわけですが、なんとこの言語キャストのオーバーロードもできてしまいます。超かっこいい。

キャスト演算子オーバーロードの例
class Foo {
public:
    Foo(int data) {
        this->data = data;
    }

    // キャスト演算子オーバーロード(オブジェクトをintに変換するときに呼ばれる)
    operator int() {
        return this->data;
    }
    
private:
    int data;
};

int main(void){
    Foo f = 123;
    int a = f; // ここでoperator int()が呼ばれる
    cout << a << endl;
    return 0;
}

まあこれも自分で定義することはまずなくてライブラリで「適切に」定義されているものを使うことになると思います。昔OpenCV触ってる時に見かけた気がします。

他の例としては以下のfstreamを使ったファイルI/Oですね。

裏でキャスト演算子オーバーロードが使われています
string s;
ifstream fin("foo.txt");
while (fin) {
    getline(fin, s);
    cout << s << endl;
}

ここで重要なのはwhile (fin)の部分です。実はここでoperator bool()が働いており条件式の中にfinと書くだけでいいようになっています。参考

「キャスト」というと普通は次に説明する明示的な変換の話ですがC++では暗黙的に変換もされることがあるのでここで紹介しました。

明示的な変換

上記のいろいろな暗黙変換を許さない言語、あるいは暗黙変換が許される場合でも可読性等の理由により変換を明示するときに使うのがキャストです。だいぶ上の方に書いたJavaではエラーになる「int変数にdouble値を入れる」もキャストをしてやればOKです。もちろん小数点以下は切り捨てられます。

キャストすればOK
int a;
a = (int)123.45;

PHPの話のついでに書いた「文字列と数値の足し算を許さない言語」でも数値側を明示的に文字列にしてやれば文句は出ません。以下の例はPython

文字列にすれば足し算(連結)化
"12" + str(3)

オブジェクトのキャストについて、はおもしろくないので略しますね。令和にもなってダウンキャスト使ってる人っているんですか?

C言語のキャスト

初めに目次考えたときにこの項目を挙げていたのですが何の話をするのか忘れてしまいました(ぉぃ
普通のキャストの話なわけはないから構造体のキャストの話だったかな。Ruby1.9時代に感動した構造体キャストの妙技があったのですが今では実装も変わってるようなので紹介するのはやめてきます。

おわりに

以上、プログラミングの一番初めに習う「変数への代入」とそれに付随する各種変換処理についていろいろとお話ししてきました。普段何気なく書いている代入処理も考えてみると奥が深いなと思っていただけたのなら幸いです。

  1. 言い訳すると、パッケージをインポートする部分でソースに沿ってバイナリを手動解析しているうちにめんどくさくなってしまいました。

  2. 構造体と構造体に対するメソッド定義でクラスのようなことはできます。さらに「構造体埋め込み」という裏技みたいな方法で継承っぽいこともすることはできます。

  3. Static Single Assignment。CPUアーキテクチャ別の機械語にする前の中間言語らしいです。

  4. アセンブリに変換してみたらcvttsd2siという命令が使われていました。SSE2で追加された命令のようですね。ここら辺、プログラミング言語間だけでなくコンパイラ(とプロセッサ)自体の歴史も眺めるとおもしろいですね。参考記事

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?