Nanoのソフト担当です。前回 の①に続き、それっぽいことを書きます。今回は、C++の唯一好きな機能である、"constexpr"("こんすといーえっくすぴーあーる"と読むらしい。ぶっちゃけexprは"えくすぷら"って読んじゃう。でも実は大方の意見は全然違う) について布教もかねて書こうと思います。

まえがき

constexprについて書こうと思いましたが、Arduino IDEではconstexprが使えないことを思い出したので、使えるように改造したArduino IDEを置いておきます。

Mac OS X 用

Windowsの人は…ごめんなさい。Linux?知らんな

これを忘れて公開したのが、先日の記事公開直後に削除事件の真相です。

ステップ① constexprとは

constexprは最近のC++規格"C++11"で導入された指定子です。イメージとしては、constと#defineがいい感じに合成されたと思いきや現実は厳しかったというような感じです。

いくら説明しても意味不明なので、constexprの特徴を上げてみます。

  • コンパイル時に実行できる!!!!!!!!

実際これだけです。これがなければconstでも#defineでも使えばいいのです。詳しくみていきましょう。

ステップ② constexprでしかできないこと

最も簡単なconstexprの使い方として、配列の要素数に使用できるというものがあります。例えば、

void foo()
{
    struct Foo { int size; };
const Foo x = { 42 };
char string[x.size] = "foo"; /* なにか`string'を使った動作 */ }

このプログラムはコンパイルエラーとなります。(というより、なるはずです。実際はコンパイラ次第でコンパイルできます。)しかし、constexprを使うと、この問題を回避できます。

void foo()
{
    struct Foo { int size; };
constexpr Foo x = { 42 };
char string[x.size] = "foo"; /* なにか`string'を使った動作 */ }

最近はコンパイラの頭がよくなってきたので、この手の問題の回避手段としてconstexprを使う機会はなくなってきているように感じます。

ステップ③ 真・constexprでしかできないこと

大変長らくおまたせいたしました。ここからが本編です。先ほどのコードではconstexprを変数に指定していましたが、関数に指定すると、コンパイル時に関数呼び出しできるようになるのです。

例:二乗を求める関数

ある数`x'の二乗を求める関数を定義したいとします。普通に書くと、

int square(int x)
{
    return x * x;
}

みたいな感じのコードになるでしょう。でも、関数呼び出しだってタダじゃない。それなりのコストが伴います。ちなみに、うちのハード担当はタダです。

ある程度Cがわかってくると、ここで`inline'を使うようになります。そうすれば、関数呼び出しのオーバーヘッドがなくなるからです。ただし、汝、コンパイラを信用するなかれ。日頃の行いが悪いとコンパイラは関数をインライン展開してくれません。でも大丈夫。`__attribute__((always_inline))'という呪文を唱えれば必ず展開してくれます。長いので使うときはマクロにしてしまいましょう。

#define INLINE inline __attribute__((always_inline))
INLINE int square(int x)
{
    return x * x;
}

関数呼び出しのオーバーヘッドもなくなり、一見完璧にみえます。しかしこの関数を、

square(2)

のように呼び出した場合はどうでしょう。この式の値はどう考えても4です。しかしこのままでは、実行時にいちいち計算してしまいます。

ならばとマクロとして定義したらどうでしょう。

#define square(x) ((x) * (x))

これもまずいです。例えば、

square(analogRead(A1))

というふうに呼び出すと、2回analogRead(A1)を呼び出してしまいます。analogRead()だってタダじゃない。これなら先ほどのようにインライン展開した方が断然速いです。しかも、二回目の呼び出しでは値が変わってしまい、二乗にならない可能性も出てきます。ちなみに、うちのハード担当はタダです。

大正義constexpr

このままでは二乗すら求められなくて終わってしまいます。どうすればいいのでしょう。そう!constexprを使えばいいのです。

#define INLINE inline __attribute__((always_inline))
INLINE constexpr int square(int x)
{
    return x * x;
}

この関数に定数を与えて呼び出すと、コンパイル時に実行され、変数を与えるとインライン展開されて呼び出されます。

この程度の関数ではあまりありがたみが感じられないので、もう少し実用的な例を出して考えましょう。

例:sinを求める関数

三角関数は制御をする上で欠かせません。しかし、その演算には時間がかかります。ここではそんな三角関数のsinを高速に求めてみようと思います。

sinを求めるにはまず\(\sin x\)にマクローリン展開という数学的処理を施します。詳しい説明は省きますが(高専一年生の段階ではとても判らない)、こんな感じになります。

\[\sin x = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \frac{x^9}{9!} …\]

こんな感じで永遠と続いていきます。もう少し数学的に書くと、

\[\sin x = \sum_{n=0}^{\infty}(-1)^n\frac{x^{2n+1}}{(2n+1)!}\]

こう表記できます。このnに0から無限大までを代入して足しあわせていけばいいわけですね。

式も判ったので、C++で実装してみましょう。お判りかと思いますが、nのとる値には限界があります。n=10くらいまで計算すれば充分でしょう。

必要な関数その① pow()

まずは、

\[x^{2n+1}\]

をC++で実装します。この役割を果たす関数にはすでに、

double pow(double __x, double __y)

がありますが、この関数は非constexprです。constexprな関数で、非constexprな関数を使ってはならないのです。非constexprな関数はコンパイル時に実行できないことを考えれば、当然の帰結ですね。

ということで`x'の`y'乗を返す関数myPow()を定義します。

constexpr double myPow(double x, int y)
{
    if (y == 0) return 1;
    for (int i = 1; i < y; i++) {
        x *= x;
    }
    return x;
}

このプログラムは完璧です。C++の最新規格C++14において、なんの不足もありません。しかし、このプログラムはArduinoで使われるavr-gccではコンパイルができません。avr-gccは、C++14の機能を完全に実装していないのです。Appleなどが中心となって開発しているコンパイラclangのソースコードを改造して、avr-clangを作ればコンパイルできますが、(かなり大変でしたが、実際正常に動作しました)それを書くと誰もが消化不良になって終わるので、別のアプローチを考えます。

コンパイラの限界

avr-gccにおいて、constexprな関数には、以下の形の文しか書けません。

return A;

`A'には何を書いても構いませんが、重要なのは、セミコロンは一個までということです。そこで、条件分岐には、

return 条件 ? 真 : 偽;

の形の文を利用し、ループには再帰呼び出しを利用します。そうしてできた新しいmyPow()が以下のコードです。

constexpr double myPow(double x, int y)
{
return y == 0 ? 1
:
y > 1 ? x * myPow(x, y - 1) : x;
}

必要な関数その② factorial()

今度は、

\[(2n+1)!\]

を実装します。記号"!"は階乗を表します。階乗とは、

\[5! = 5 \times 4 \times 3 \times 2 \times 1\]

こんな感じの計算です。

constexpr unsigned long factorial(int x)
{
    return x > 1 ? x * factorial(x - 1) : 1;
}

せっかくなので safeRadians()

せっかくなので、360度法の値を弧度法の値(ラジアン)に変換する関数も作ります。単に\(\frac{\pi}{180}\)をかけるだけでなく、\(-90\leq x\leq 90\)の範囲内に角度を収めるようにするので、安全です。constexprなので、実行時のオーバーヘッドの心配もいりません。

constexpr double safeRadians(int degrees)
{
    return degrees > 90 && degrees <= 180 ? radians(180 - degrees) :
    degrees > 180 && degrees <= 270 ? radians(180 - degrees) :
    degrees > 270 ? safeRadians(degrees - 360) :
    degrees < -90 ? safeRadians(degrees + 360) :
    radians(degrees);
}

いよいよ! mySin()

後は簡単です。今までの関数を組み合わせて、mySin()を作りましょう。

constexpr double mySin(double x, int i = 10)
{
    return i == 0 ? x :
    (i % 2 ? -1 : 1) * myPow(x, 2 * i + 1) / factorial(2 * i + 1) + mySin(x, i - 1);
}

これを、

mySin(safeRadians(90))

のように呼び出せば、めでたくコンパイル時sinの完成です。mySin()とsafeRadians()の引数には、 リテラル値(90、1.57など)または、constexprな定数を指定できます。それ以外を指定しても動きますが、その場合はかえって遅くなるので注意してください。

mySin(safeRadians(x)) \(-720 \leq x \leq 720\)

この条件でグラフを作ってみました。

sin
しっかりと正弦波が出ています。

ちょっと長い記事になりましたが、ここまで読んでいただきありがとうございました。みなさんが良きconstexpr人生が送れますように。