Metashellを使ったC++メタプログラミングの入門とデバッグ
この記事は
C++ Advent Calendar 2015の第1日目です。 今年は初心者向けのゆるい記事を書くための初心者 C++ Advent Calendar 2015も あるにも関わらず、どうやら埋まりそうです。参加者の皆様ありがとう。
この記事ではMetashellの使い方とメタプログラミングの初歩をやります。
対象としてる読者
Metashellとは何か
本家ドキュメントのMotivationをざっくり意訳します(翻訳とはいってない)。
When one starts learning a new programming language such as Haskell, Python or Erlang an interactive shell (
python
for Python,ghci
for Haskell,erl
for Erlang) is available for experimenting with the language. Later, after getting familiar with the language, this shell becomes a tool to try code out, verify test results or learn more about the problem during debugging.
対話型環境(例えばHaskellのghci
, Pythonのpython
, Erlangのerl
)を使うと、
色々と試しながらプログラミング言語を習得することができます。習得後でも、
ちょっとしたコードを実行したり、その結果を確認したり、あるいはデバッグの際にも
対話型環境は役立ちます。
When one starts learning C++ template metaprogramming, he has to use a C++ compiler which was designed to compile code and not to play with template tricks. People new to template metaprogramming have to learn how to make the compiler show them the result of simple metaprograms. This makes it more difficult to get started with template metaprogramming.
C++のテンプレートメタプログラミングを学び始めるには、 コンパイラを使ってコードを試しますが、コンパイラはコードをコンパイルするためのもので、 テンプレートで色々と遊んでみるものではないのです。 入門者はまず、簡単なメタプログラミングの結果を 「どのようにしてコンパイラに表示させるか」から学ばなければなりません。 このためメタプログラミングの習得は難しいのです。
Later, after getting familiar with template metaprogramming, developers have to use the compiler for testing and debugging. Understanding what is going on in a template metaprogram during debugging is difficult, since the compiler needs to be forced to present the result of a metaprogram (or a part of it) - and compilers are not prepared for this. This extra difficulty in debugging makes simple problems look difficult.
テンプレートメタプログラミングに慣れた後でも、開発者はテストやデバッグにコンパイラを使わなければなりません。 デバッグのためには、コンパイラにメタプログラムの結果(あるいはその一部)を表示させますが、 コンパイラはそのような用途を想定して作られてはいないので、簡単なはずの問題が複雑に見えてしまうという余計な問題が生じます。
Metashell provides an interactive shell similar to the Python, Haskell and Erlang shells for template metaprogramming. It uses Clang to evaluate the metaprograms.
Metashellはテンプレートメタプログラミングのためのシェルです。Python, Haskell, Erlangのシェルと同じような対話型環境です。Metashellはメタプログラムを評価するのにClangを利用しています。
C++メタプログラミングのハードルが高いのはなぜか
上記本家ドキュメントのMotivationを要約すると、ちょっとコードを試してみようとしても毎回コンパイルかけなければならず、しかも結果を確認するのも手間がかかると。だからコンパイルかけなくてもコードを試せるメタプロ用の便利なシェルを作りましたということですね。 メタプロが難しい原因はこれだけではないですが、理由の1つではあります。
Metashellの特徴
- 対話型環境(REPL)なので、結果をすぐ確認できる。
- Windows,Mac,Linux,BSD(FreeBSD,OpenBSD)で使える。
- オンライン版ならば今すぐブラウザで使える。
- ライセンスはGPL3
インストール方法
基本的にはここ読めば書いてあります。 まず試してみたい人はOnline版があるのでとりあえずインストールしなくてもいいです。
Windows
何も考えずにバイナリインストーラ使いましょう。オプション全部デフォルトのままでインストールすればいいです。
Mac
Homebrewでインストールできるそうです
$ brew install metashell
Linux
Ubuntu, Debian, Fedora, OpenSuSEしか公式バイナリは用意されてないみたいですが、それ以外のディストリでもコンパイルすれば使えると思います。私はArch Linuxで素直にコンパイル通った・・気がします(覚えてない)。コンパイル時間は長かった気がしますので覚悟。Building manuallyのところ読めばLinux使ってる人はたぶん普通にできるので解説省略。
起動方法
インストールした場合
Windowsをターゲットに解説。基本的にはLinuxでもMacでも同じです。
インストールするとスタートメニューに出てくるのでクリックすれば起動できますが、この方法ではBoostとかのヘッダパスを探してくれません。しかもC++0xモードになってしまいます。なのでコマンドプロンプトからオプションつきで起動させましょう。
"C:\Program Files (x86)\metashell 2.1.0\bin\metashell.exe" --std c++14 -I C:\Users\ignis\Documents\boost_1_59_0\boost_1_59_0
パスは適宜書き換えてください。 -I
以降はBoostを利用しない場合は不要です。
こんな感じで起動するはずです。わからなければとりあえずOnline版を使いましょう。
Online版を使う
Metashell demo から Start Metashellに行けば使えます。C++14オプションはいれましょう。
メタプログラミング入門
簡単なコンパイルタイム計算をやってみます。本家ドキュメントではBoost.MPLを使ってますが、ここでは標準ライブラリのみで進めます。
整数を型で表そう
std::integral_constant
まず std::integral_constant
を使って整数を型として表現します。integral_constant
はC++11から標準ライブラリに入りました。type_traits
ヘッダをincludeすることで使えるようになります。典型的には
template<class T, T v> struct integral_constant { static constexpr T value = v; typedef T value_type; typedef integral_constant type; constexpr operator value_type() const noexcept { return value; } constexpr value_type operator()() const noexcept { return value; } //since c++14 };
と定義されます。C++メタプロ入門者はまず、static constexpr T value = v;
に注目してください。例えば、
std::integral_constant<int,4>;
は、int
型で値が4
というvalue
を内包する型になります。
metashellで見てみる。
まずはヘッダファイルを読み込みます。
> #include<type_traits>
次に、integral_constantを使って整数を型として表現します。
> std::integral_constant<int,4>
すると次のOutputが出ます。
std::integral_constant<int, 4>
この型のvalue_type
はint
のはずです。確認してみましょう。
Input:
> using four = std::integral_constant<int,4>; > four::value_type
Output:
int
このようにMetashellでは、型を入力すると即座に結果が見られるので非常に便利です。
値を表示するには
Metashellは型を表示させることはできますが、値を表示させることはできません。したがって、
decltype(3)
はint
と表示されますが、
> 3
は型ではなく値なので、エラーとなります。これはなかなかに不便なので、metashellは値を型に変換するSCALAR
マクロを用意しています。
Input:
> #include<metashell/scalar.hpp>
> SCALAR(3)
Output:
std::integral_constant<int, 3>
足し算を定義してみよう
整数を表す型が表現できたので、次はその型同士のコンパイルタイム足し算を作ってみます。
Input: (※metashellでは複数行の入力は行末にバックスラッシュをいれます)
> template<class L, class R> struct plus { \ ...> using type = std::integral_constant< \ ...> decltype(L::value+R::value), \ ...> L::value+R::value>; \ ...> };
LとRにはstd::integral_constantが入ります。LとRそれぞれが持つ値は::valueで取れますので、足し算は
L::value + R::value
です。足し算の結果の型は少し厄介です。
int
とint
の足し算の結果はint
ですが、実際に渡される型はint
とは限らないため、
decltype
で足し算の結果となる型を得ています。
足し算ができるか、確認してみます。
Input:
> using one = std::integral_constant<int,1>; > using five = plus<one, four>::type; > five
Output:
std::integral_constant<int, 5>
できました!
0から10までの和を計算してみよう
ループに相当する処理を含むメタプロです。和の計算は
とかけるので、これをこのまま再帰で計算すれば良さそうです。N=0を特殊化することで終了条件にできます。
Input:
template<int N> struct sum { using type = std::integral_constant< decltype(N+sum<N-1>::type::value), N+sum<N-1>::type::value>; }; template<> struct sum<0> { using type = std::integral_constant<int,0>; };
試してみましょう。
Input:
sum<10>::type
Output:
std::integral_constant<int, 55>
フィボナッチ数列を計算してみよう
(書く時間が足りなくなってきたのでやめます)
メタプログラミングのデバッグ
メタプロ初心者だとすでに難しくなってるかもしれないので、デバッガを使って1ステップごとに処理を確認してみます。
デバッグモードに入ります。
Input:
#msh mdb sum<10>::type
Output:
(mdb)
(mdb)という表示になったら、デバッグモードです。デバッグモードをやめるにはquit
を入力します。
1ステップ毎に実行する (Stepping)
2回ほどstep 1
と入力してみましょう。
(mdb) step 1 sum<10> (TemplateInstantiation from <stdin>:2:26) (mdb) step 1 sum<9> (TemplateInstantiation from /tmp/just-hiSTUS/metashell_environment.hpp:14:77) 12 #include<utility> 13 #include<metashell/scalar.hpp> -> 14 template<int N> struct sum { using type = std::integral_constant<decltype(N+sum<N-1>::type::value), N+sum<N-1>::type::value>; }; 15 template<> struct sum<0> { using type = std::integral_constant<int,0>; }; 16 #include<metashell/scalar.hpp> (mdb)
一度目のstep 1
でテンプレートのInstantiationが始まります。2回目のstep 1
ではsum<9>
がtemplate<int N> struct sum
で呼ばれています。10+sum<9>::type::value
のところですね。その後何度もstep 1
を続けていくと、
sum<0> (Memoization from /tmp/just-7ReGgQ/metashell_environment.hpp:14:77)
とでます。Memoizationというのは、雑に説明すると1度評価したことがあるので、以前の計算結果を使うという意味です。sum<0>は厳密に言えば評価済みではありませんが、特殊化してあるのでMemoization扱いになります。
このようにMetashellでは1ステップ毎に(もしくはnステップ毎に)メタプログラムを実行していくことができます。
やってから気づいたのですが、単純な総和だとデバッガ走らせても面白くない。。
指定したところで中断させる、そして再開させる(Breakpoints and continue)
sum<0>が呼ばれるまで実行させたい場合はブレークポイントを使います。まずはメタプログラムの最初の実行まで戻ります。
(mdb) evaluate Metaprogram started
もしくは単純にe
でも良いです。
sum<0>が呼ばれたら停止する、というブレークポイントを設置します。
(mdb) rbreak sum<0> Breakpoint "sum<0>" will stop the execution on 2 locations
2箇所で停止するようです。では実行させてみましょうcontinue
もしくはc
コマンドを使います。
(mdb) continue Breakpoint "sum<0>" reached sum<0> (Memoization from /tmp/just-nOimAz/metashell_environment.hpp:14:77) 12 #include<utility> 13 #include<metashell/scalar.hpp> -> 14 template<int N> struct sum { using type = std::integral_constant<decltype(N+sum<N-1>::type::value), N+sum<N-1>::type::value>; }; 15 template<> struct sum<0> { using type = std::integral_constant<int,0>; }; 16 #include<metashell/scalar.hpp>
きちんとsum<0>の呼び出し時でデバッガが停止しました。
実行履歴をみる (BackTrace)
ブレークポイントで停止した位置までの履歴をbt
コマンドで見ることができます。
(mdb) bt #0 sum<0> (Memoization from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #1 sum<1> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #2 sum<2> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #3 sum<3> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #4 sum<4> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #5 sum<5> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #6 sum<6> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #7 sum<7> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #8 sum<8> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #9 sum<9> (TemplateInstantiation from /tmp/just-nOimAz/metashell_environment.hpp:14:77) #10 sum<10> (TemplateInstantiation from <stdin>:2:26) #11 sum<10>::type
一番上が現在位置(breakpoint)一番下がメタプログラムの最初です。順番にN-1しながらsumが呼ばれています。
(Forwardtrace)
バックトレースの逆です。これから実行されるメタプログラムを順番に表示していきます。 以下、記事を書く時間が尽きましたので、またの機会に加筆します(やらないパターン)。
Profiling
Formatter
次は
adatcheyさんの C++ - Variadic Template を使って switch を使ったテンプレート関数呼び出しを除去する - Qiita
です。