読者です 読者をやめる 読者になる 読者になる

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.

対話型環境(例えばHaskellghci, Pythonpython, Erlangerl)を使うと、 色々と試しながらプログラミング言語を習得することができます。習得後でも、 ちょっとしたコードを実行したり、その結果を確認したり、あるいはデバッグの際にも 対話型環境は役立ちます。

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を利用しない場合は不要です。

f:id:ignisan:20151201180909p:plain こんな感じで起動するはずです。わからなければとりあえずOnline版を使いましょう。

Online版を使う

Metashell demo から Start Metashellに行けば使えます。C++14オプションはいれましょう。

メタプログラミング入門

簡単なコンパイルタイム計算をやってみます。本家ドキュメントではBoost.MPLを使ってますが、ここでは標準ライブラリのみで進めます。

整数を型で表そう

std::integral_constant

まず std::integral_constant を使って整数を型として表現します。integral_constantC++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_typeintのはずです。確認してみましょう。

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です。足し算の結果の型は少し厄介です。 intintの足し算の結果は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までの和を計算してみよう

ループに相当する処理を含むメタプロです。和の計算は

{ \sum_{i=0}^{N} = N + \sum_{i=0}^{N-1} }

とかけるので、これをこのまま再帰で計算すれば良さそうです。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

です。