C++ はながいこと食わず嫌いだった。とにかく「難しい」というイメージだけ先行していた。
しかし、あくまで better C として使う限りでは難しくないし、可読性が上がるので、 C++ を使わない手はないという気持ちになった。
これまでの漠然とした C++ への不安
書いたコードと出てくるバイナリ
C を書いていると、だいたいは書いたコードがそのままバイナリに翻訳されている感があり、安心感がある。struct はメモリ上の配置そのままだし、関数だって単に処理のまとまりに名前がついてて、呼び出しもとに自動的に戻ってくるラベルなだけだ。
C++ になるとオブジェクト指向に概念が入ってきて、これが実際どのようにコンパイルされるかに不安が出てくる。クラスはメモリ上でどう表現されているのか、メソッドディスパッチはどのように行われているか?
もちろん知っている人にとっては簡単な話で
- class は単に struct である
- フィールド定義はそのままメモリ上の表現となる
- メソッド定義は関数になる
- 実際、C++ で class と書かれた定義を struct に置き換えても問題なくコンパイルされる (フィールドのスコープがデフォルトで class は private で struct が public である違いだけ)
- メソッドディスパッチは基本的に静的である
- レシーバによって動的にディスパッチされる関数は virtual 関数と呼ばれる
- コンパイラがテーブルを作って動的に呼びだし関数を変えている
ということで、virtual を使わない限りでは、C++ の class は C で struct 定義と、その第一引数にその sturct ポインタをとる関数郡でしかなく、これは C でよくやるオブジェクト指向のプリミティブな実装とよく似ている。
このようなCのコードは
#include <stdio.h>
typedef struct {
unsigned counter;
} my_counter;
void my_counter_init(my_counter* this) {
this->counter = 0;
}
void my_counter_incr(my_counter* this) {
this->counter++;
}
unsigned my_counter_get_count(my_counter* this) {
return this->counter;
}
int main() {
my_counter counter;
my_counter_init(&counter);
printf("%d\n", my_counter_get_count(&counter));
my_counter_incr(&counter);
printf("%d\n", my_counter_get_count(&counter));
}
このようなC++のコードとほとんど同じバイナリが出力される
#include <cstdio>
class my_counter {
unsigned counter;
public:
my_counter(); // void init();
void incr();
unsigned get_count();
};
my_counter::my_counter() {
this->counter = 0;
}
void my_counter::incr() {
this->counter++;
}
unsigned my_counter::get_count() {
return this->counter;
}
int main() {
my_counter counter;
printf("%d\n", counter.get_count());
counter.incr();
printf("%d\n", counter.get_count());
}
テンプレート多用するとバイナリサイズ増えるんじゃないの?
テンプレートは、複数の型に対する処理を1回でまとめて書けるという仕組みなので、原理的には、扱う型が増えるほど、出力バイナリに関数本体が増えていくことになる。
しかし一方で、テンプレートをコンパイル時計算のために使うような場合は、コンパイル時に解決されるコードがほとんどになり、出力バイナリは書いたコードの見た目の多さに反してかなり少なくなることもある
ということで、テンプレートが使われているからといってバイナリサイズが肥大化するというわけではない。
最適化はどこまで信用できるか?
この書きかたで本当に最適化されたコードに出てくるの? という不安がある。これは難しくて、コンパイラが優秀でも、当然プログラマがちゃんと const を付けてコンパイラに意図を伝えないと、完全に最適化されたコードにはならない (インライン化されないとか)。
C にはない概念がある分、const の付けかたが複雑で、ベターCとして使おうと思うと一番ハマる。
一方で、テンプレートメタプログラミングでコンパイル時に計算することができるので、コンパイラが判断できないような高度な最適化を自力でやることができる。
なぜ組込みでこそなのか?
組込みのコードはどうしてもマジックナンバーが多くなりやすく、C で書くとマクロだらけになる。Cのマクロは文字列展開なので型がなく、当然マジックナンバーにも型を付与できない。組込みは実行時デバッグのコストが高いので、コンパイル時に見つけることができるエラーは全てコンパイル時に見付けたいが、Cではそこまでのことができない。
浮動小数点まわりについてはMCUでは非常に重い処理になるので、できるだけ事前計算した係数をつかってMCU上では整数演算にしてしまうなどの最適化をしたくなる。こういうときCのマクロだけで書くのはとても厳しい。C++ であればコンパイル時に浮動小数点計算を行って定数展開できるので、あきらかに有利になる。
浮動小数点に限らず、高いレイヤーでの最適化はコンパイラは知るよしもないので手でやる必要がある。その際C++のTMPは非常に強力に使える。