できます。(一部地域を除く)
……どうも。14らりおです。
お分かりの方も多いかと思いますが、一部地域というのはM$帝国です。
とはいえ今回のテクニックにはC++14という最新(2015年現在)の規格の機能を使っており、
V○++とかgcc5未満とかclang3.5未満とか、C++14非対応のコンパイラも残念ながら使えません。
以下に載せるサンプルはgcc-5.1以上か、clang-3.5.0以上で動かしてください。
(C++11のソースならgcc-4.8とかclang-3.3とかからいけたはずです。V○++は知りませんが、お察しお試しください。)
追記: 2015/07/20にリリースされたらしいVS2015からは、C++11のconstexprが使えるようになったらしいです。こんぐらちゅれーしょん!
(詳細はコメントを参照のこと。)
あと、C++14に特に興味ないという人はここから読んでください。
見た目
ヒュー!超クール!
つかいかた1 (あまりよくないけど超シンプル)
文字列リテラル(「"string"
」みたいにダブルクォートで括って書く文字列定数)の後ろに「_crc32
」と付けると、あら不思議!CRC32で変換された32ビット整数(CRC32ハッシュ)が出てきます。
32ビット整数ということは……そうです、switch-caseで使えます。
リテラル以外の文字列も大丈夫です。nu11p0::crc32()
関数を使ってやれば、変数でもCRC32ハッシュに変換できます。
この使い方にはちょっとだけ問題があって、稀に「異なる文字列に同じハッシュが割り当てられる」という現象が起きます(これを「衝突」といいます)。
喩えて言うなら、「フルネームが異なる複数人に対して同じ呼び名が割り当てられてしまう」といった感じでしょうか。
ちなみにロ技研で「おい○リコン!」と呼ぶと複数人が反応します。
このコードのように
運よくエラーになれば良いのですが、
普通はこっちのコードみたいに、
「buckeroo」という入力に「plumless」のcaseが反応してしまう、という危険性があります。
(CRC32ではこのような衝突を意図的に起こすことができるようで、脆弱性になりえます。)
つかいかた2 (チェックすればおkだけどちょっと汚い)
caseでマッチした後に改めて文字列全体を確認してやれば、つかいかた1のような偽陽性の反応は回避できます。
うーん、ちょっと面倒です。
こんなの何度も書きたくないですね。
つかいかた3 (冗長な記述を減らした)
困ったときのマクロです。
まあ、見れるレベルではあります。
が、たとえばCASE("foo")
が実質的にif文だから後に2文以上続けるなら波括弧が要るとか、普通のcase文と記法や挙動が違うとか、break文をその括弧の外に出さないとdefaultまで降りていってしまって効率が落ちたり、というような不満はそれなりにあります。
そうそう、default文もこのままだと書けません。
もし上の方のcaseで衝突として弾かれた場合、breakしてしまうので、たとえば"plumless"のcaseがあった場合、入力が"buckeroo"でもdefaultに到達しません。
(↑hashTest3("buckeroo");
で何も(defaultの出力も)出てこない!)
つかいかた4
こういうときにはフラグを使えばおkです。 これで出力も意図したとおりになります。 やったね!
具体的な利用例
私はXMLパーサを書こうとしてこのコード(ライブラリ?)を書きました。
XMLのタグ名でswitchさせます。
このような、分岐先の語彙が固定である場合において、switch-caseは非常に良い選択肢です。
解説
一番重要なコードはcrc32_literal.hppなんですが、泥臭いしC++というよりCRC32の問題だし、ここで詳らかに解説するようなことでもありません。
強いて言うなら、
149行目あたりのコレが一番重要なんですが、見ればなんとなくわかると思うので詳しくは書きません。
解脱
このヘッダでは以下のような、C++11やC++14の新しい言語機能を使っています。
relaxed constexpr restrictions (constexpr関数の制限緩和)
コンパイル時にさまざまな処理ができるようになりました!CRC32の計算とか!
user-defined literals (ユーザ定義リテラル)
数値とか文字列の後に「_foobar」みたいな感じのsuffix(接尾辞)を付けることで、型を変えたり値を変えたりできます。
文字列を数値に変えるのもコレを使いました。
std::array
Cの配列をC++っぽくしたやつです。
普通に使うと有り難みが少ないですが、at()
メンバ関数とか、Cと違って関数に渡しても長さの情報が消えないとか(Cの配列は関数に渡すとsizeof
の挙動が配列でなくポインタ向けになってしまいます)、data()
があるからvectorと同じように使えるとか、まあいろいろメリットがあります。
static_assert
コンパイル時計算や型の条件などのチェックをコンパイル時にできます。
メタプログラミングなどにも重宝します。
数値リテラルの桁区切り
0x12345678
のように長いやつを0x12'34'56'78
のように書けます。
それだけです。
とはいえ64ビット整数とかは桁数も大きくなりますし、今回は使っていませんが2進数リテラル(0b0100
みたいな)も使えるようになったので、地味に便利です。
initializer_list, range-based for
これはヘッダではなくcppファイルの方で使ったテクニックです。
initialier_listは本来の使い方ではないのでここでは解説しません。
range-based forは、簡単に言えばfor-eachみたいなもんです。
これを組み合わせると、
これが
こうなる感じでコードが書けます。
めっちゃクール。
無難で普通なやりかた (C++11)
熟々と(←読める?)書きましたが、たぶん上のようなのは物好きな人しか使わないんじゃないでしょうか。
switch文ではありませんが、C++11の機能で似たようなことができます。
ま、これが一番無難ですね。
初期化のメモリアロケーションとか呼び出しのstd::function
のオーバーヘッドでちょっとコストがかかりますが、この程度なら気にしないのが良いでしょう。可読性の方が大事です。
unordered_map
も内部でハッシュを使っていますので、要素数が大きければif-elseの連鎖を大量に並べるよりマシなパフォーマンスになるかもしれません。
しかもハッシュの衝突は気にしないでよし。ありがたや〜。
ちなみに、unordered_map
はconst
にしなかった場合、動的に処理や選択肢を変更できるので、switch-caseより高機能です。
結論
C++14おもしろい。
でもパフォーマンスへの執念がないならunordered_map
使っとこう。
あとVC○+じゃなくて、ちゃんとC++11対応したコンパイラを使おう。