Javaのenumとswitchとコンパイル結果
目的
Oracleの配布しているJDKと、Eclipseで使われるJDKで、コンパイル後のファイル数が違うケースに気づいたのでメモ。
switch文でenumを使う時のコンパイル結果についてです。
本題
Javaのenumが、ただの定数ではなくて特殊なクラスとして扱われるのは有名なことかと思います。EffectiveJava(読みかけ 汗)にも書いてありました。
クラスなので、コンパイルすればenum定義は一つの.classファイルになります。クラス内で定義したら、staticな内部クラスとして、やっぱり別の.classになります。
ところで、そんなenumの利点として、switch文の条件にできるという点があります。
では、このようなコードはコンパイルするとどういったファイル名になるでしょうか?
Person.java
public class Person { public void work(Day day) { switch (day) { case SAT: System.out.println("寝て過ごす"); break; case SUN: System.out.println("遊びに出かける"); break; default: System.out.println("働く"); break; } } }
Day.java
public enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
Eclipseのコンパイル結果は予想通り
自分は、当然Person.classとDay.classの2つになると思ってました。
そして、Eclipseを使うとその通りになります。(バージョンは4.2と4.5で確認)
Oracle JDKでは違った
でも、OracleのJDK(1.7)のjavacを使ったら、
* Day.class
* Person.class
* Person$1.class
の3つになりました。なぜだ(・д・)
javap Person$1.class とすると、
Compiled from "Person.java" class Person$1 { static final int[] $SwitchMap$Day; static {}; }
だそうな???
検索すると次のような記事に当たります
enum 定数の switch 文の実装 - happynowの日記
Java列挙型メモ(Hishidama's Java enum Memo)
enumの値が変わったり増減したりしても、enumを使う側はコンパイルし直さなくても済むというのがenumの特徴です。そういった挙動を既存の文法だけで再表現するための実装として、Oracleのjavacでは内部的に勝手にクラスを作っていたんですね。そしてEclipseのコンパイラでは別の実装になっていると。
感想
Javaではクラスごとに.classファイルが作られることになっています。
しかも無名のクラスは$1のような通し番号のついたクラスになったりします。
実装依存なのだとしても、コンパイル後のファイル数まで違ってしまうのはなんとも気持ち悪いというか。
たとえば1つの.javaファイルから100個の.classが作られることも許されているのかなみたいな。
コンパイル後のことなんて気にするなと言われれば、まぁそうなんですが...('_')ナンダカナー
全然別の話題ですが
またオーボエはじめました。
VS2015ならconstexprでコンパイル時演算でき…ない??
事の経緯
前回・前々回と、C++のTemplateを使った処理について書きました。 そして、以前知人が
まだ実行時ソートで消耗してるの? 〜ScalaでHListを使ったコンパイル時クイックソート 〜 (前編) - だいたいよくわからないブログ
とかいう記事を書いてるのを思い出し、C++でもできるかなーと考え始めたわけです。(某煽り記事が話題になった時に書かれたのでタイトルはあれですが、中身はまじめな技術記事ですので念のため・ω・)
はじめはTemplateで上の記事の内容を真似ようかなと思ったものの、最近のC++ならconstexprがあるからソート処理くらいならそれでできそうだなと考え始めたわけです。
constexprとは
…いろんな記事が出てるので、そちらを見てください。自分もちゃんと使おうと思ったのは初めてなので。。
Visual Studio 2015でも使えるよね ってことはMicrosoftの出してる情報で確認しました。
コンパイル時演算の確認方法
constexprは、コンパイル時に引数の値が決定できる場合はコンパイル時に処理を行います。 一方、実行時まで引数の値が分からない場合には、普通の関数のように振る舞います。 そのため、コンパイルが通ったからといってそれだけではコンパイル時演算できている保証はできません。。
コンパイル時に演算できているかの確認は上記情報によると、
注: Visual Studio デバッガーでは、内部にブレークポイントを挿入することで、constexpr 関数がコンパイル時に評価されているかどうかがわかります。 ブレークポイントにヒットすると、実行時に関数が呼び出されます。 ヒットしなければ、コンパイル時に関数が呼び出されます。
とのこと。翻訳前の英語も確認したけれど、ちゃんと同じ意味のことが書いてありました。
うまくいかない…??
ソートには比較が必須ですからね、まずは値の大小比較をする関数を準備せねば。 ためしに以下のようにコードを書き、実行してみました。 ブレークポイントで止まらないことを確認すればいいんだな。
F5をポチッ
……止まった( ・_・)??
おかしい、コンパイル時に値が計算されていたらここに入るはずがない…。
原因として思いついたのは2点
1つめのDebugモードだからというのは有力だけど、このくらい簡単な計算だとconstexpr指定にしなくても最適化されてインライン展開されてしまうので、ブレークポイントで確認ができないという。 (だったらconstexpr指定しなくても良いという話もある。)
2つめのVC++のコンパイラだからというのは、別のコンパイラで試せば分かるはず。 今のVS2015はコンパイラとしてClangを使うことができる。そちらに切り替えると、たしかにブレークポイントで止まらないのでコンパイル時に演算できてそうな雰囲気。
まとめ
少なくともVC++15のコンパイラでは、Debugビルドだとconstexprでも実行時処理になるみたい。
何がいけないのか知ってる人がいたら教えてほしいです。
引数が数値がかどうか判定(SFINAE)
前回に引き続きC++の型まわりで。
整数でも実数でも良いから、とにかく数値を引数にとりたいときとかってありません?自分は時々あります。画像処理をしている時とか。
常にdoubleで扱うのもいいですけど、せっかく整数で扱えるなら整数のまま扱いたい。でもかといって、int用とdouble用みたいに分けたくない。そんなもやもやな時が。
やりたいこと
引数が数値かどうかによって処理を分岐する。
実行時ではなくコンパイル時に分岐をする。
実現方法
SFINAEという原理(?)を使い、template関数に対する以下のどちらかで実現する。
この記事では、算術型 = 数値型 とみなす。厳密には、四則演算が定義されているかどうかなので違うけど、四則演算できるならまぁ良いじゃん的な。
先にコードを
SFINAEって?
templateクラスやtemplate関数のオーバーロードの解決方法の方針。
「当てはまりそうだったから試したけどだめでした!」→「よし次試すかー」
そんな感じで順に試していくというだけで、たいして難しい考え方ではないと思う。コンパイラ自体の実装は大変そうだけど。
詳しいことは各自検索で。以下のサイトなどを参考に。
数値かどうかの判定箇所
template関数の普通のオーバーロード解決 : 上のコードでいう f()
数値の場合
関数の引数に注目。第2引数の型が、std::enable_if<std::is_arithmetic<T>::value>::type* になっていますね。
void f(T arg, typename std::enable_if<std::is_arithmetic<T>::value>::type* t = 0)
std::is_arithmetic<T>::value は、Tが算術型だったときはtrueに、それ以外の時はfalseになります。つまり、たとえばTがintであれば(コンパイルの途中で)以下のように変換されます。
void f(int arg, typename std::enable_if<true>::type* t = 0)
ところで、std::enable_if<bool>::typeはテンプレート引数がtrueかfalseかによって定義変わります。 std::enable_if<true>は(template第2引数で特別指定をしなければ)void定義として定義るので、これはさらに
void f(int arg, void* t = 0)
に変換されます。なんか見慣れた感じに落ち着きましたね。(C++ではvoid*ってあまり使わないですけど。)問題なく展開できたので、f(3)とか書かれてたらこの関数が呼ばれます。
非数値(正確には非算術型)の場合
はじめに
void f(T arg, typename std::enable_if<std::is_arithmetic<T>::value>::type* t = 0)
を適用しようとすることには変わりません。そして、Tが非算術型の場合、std::is_arithmetic<T>::valueはfalseになるので、 std::enable_if<false>::type で定義される型を求めようとします。
しかし、 std::enable_if<false>にはtypeというものが定義されていません(・ω・;)こまりましたねー
すると、コンパイラは諦めて他のオーバーロードの候補が無いか探します。他に無ければコンパイルエラーになります。これで、templateを使いつつも一部の型だけに適応することができますね!
今回は非数値でも処理ができるように、別途次のようにオーバーロードを定義しています。
void f(T arg, typename std::enable_if<!std::is_arithmetic<T>::value>::type* t = 0)
見た目ややこしいですが、std::is_arithmetic<T>::valueの結果を!で反転させています。だから、数値でないものに対してはこちらのオーバーロードが無事解決できるようになります。
template引数のオーバーロード解決 : 上のコードでいう g()
(普通の)引数は、これ以外にもオーバーロードしたいことがよくある。template引数を使って分岐した方が間違いが少なくなるし、型で分岐してることがなんとなく伝わりやすい気がする。
実装での基本的な考え方は同じなので省略。ただ、こちらではダミー変数を使ってあげないと、デフォルト引数を指定できないらしい。extern指定すれば、他ファイルに影響したり無駄なメモリが確保されたりもしないので、問題無いと言えば問題ない。
その他
数値判定以外にも、いろいろな判定をできるメタ関数が標準で揃っているみたい。以下の記事にまとまっている。
あとは、記事というかcppreferenceにはやっぱり必要な情報は載っていますね。
Type support (basic types, RTTI, type traits) - cppreference.com
まとめ
- template使うと対象の型をコンパイル時に絞り込めたりする。
- 可能性は無限大、可読性は微妙。
こちらも参照ください
C++で型クラスっぽいこと - Traits
背景
昨日(一昨日か)の型クラス勉強会に参加してきました。
ただ、残念ながら自分はScalaもHaskellもSMLも明るくないしUr/Webとかいうのは初耳だったので、整理を兼ねて++で型クラスっぽい実装を。
(C++分かるとか書いたらいろいろ飛んできそうなのでそんなことは書けないのですが)
@k_katsumiさんの発表(https://speakerdeck.com/kishikawakatsumi/type-classes-in-swift)をまねて、シーザー暗号の関数を作ってみました。
目的
- Traitsの使い方を(自分が)理解する
- 他言語での型クラスのありがたみを理解する
ソース
何やってるの?
最初にこういうコードを見た時はギョッとしました(・д・)が、昨日Swiftのコードを見た後にこれを考えたら以外とスッキリ。自分なりの考え方としては以下の通り。
- 最終的に encodeInCaesar(123) や encodeInCaesar("hoge")の形で呼び出せるtemplate関数encodeInCaesar(T t)を作りたい。つまり引数の型Tによって、内部の処理を分岐させたい。
- 対象の型ごとに処理が変わるように特殊化された型CaesarTraits<T>を用意する。encodeInCaesar(T t)の中でCaesarTraits<T>を使えば処理の分岐ができる。
- 特殊化しなかったらコンパイルエラーになるように、基本のCaesarTraits<T>では何もメンバ関数を定義しない。
CaesarTraits<T> を通してデータにアクセスするという取り決めがSwiftのprotocol定義、CaesarTraits<T>を対象の型ごとに特殊化するというのがSwiftのExtension定義に相当してます。
C++のtemplateはダックタイピング的で、使い方が同じあれば型の意味とか関係なくコンパイルできてしまいます。一方で、templateは特殊化することで、特定の型について別の挙動を与えることができます。前者がparametric polymorphism、後者がad-hoc polymorphism。encodeInCaesar()関数はparametric polymorphismを利用していて、その先では実はad-hoc polymorphismになっている。だから全体としてはad-hocに振る舞う。
という認識なんですが、合ってますかね(・o・)??
まとめ
- 落ち着いて考えたらC++でもTraits使って型クラス的なものが書けた。
- でも書くのちょっと大変だしコンパイルエラーがわかりにくくて辛いのではやく文法としてのConcept (http://en.cppreference.com/w/cpp/language/constraints) がほしい。Conceptが採用されるとコンパイラの解析が楽になってエラーも見やすくなるはずなのだけど…いつ正式採用されるのかな...
その他
Wordpress.comでたまーに書いてたんですけど、ちょっとはてなブログに移ってきてみました。間違ったこと書いてそのままなのは良くないと思うので、優しいマサカリをお願いします。