C 言語よりお得な C++ その3

(5)define
C では、例外無く、普通に define を使ってきました、しかしながら、多くのプログラミングガイドなどで、害悪が指摘されるように、C++ では define を使わなくても良い方法が提供されています。
人によっては、少しだけ注意すれば便利なので、それ程とやかく言わなくても的な事を言う人もいますが、define より安全で、優れた機能が提供されているのに、何故、危険で制限のある古い方法を使うのか理解に苦しみます。
※厳密には define を使わないと、どうしても解決出来ない事が全く無い訳では無いのですが、それは、「稀」な事と思います、普通は、使わなくても何とかなる事の方が多いハズです。
※「郷に入れば郷に従え」

C では、define をアセンブラのマクロ命令のように、人間による最適化を施した拡張命令のように捉えられていると思われますが、「最適化」はいくつかの例外を除いて、人間がやるよりコンパイラに任せた方が安全で確実であると思います。
※精妙な最適化は、それが必要になったら行うべき事項であり、開発途中の中途半端な段階で行うのは問題が多く実りが少ないと言えます。

C++ では、最大の最適化でも、「関数の呼び出し」を尊重してコンパイルします、その関数が少ないステップで構成されている場合でも、指示の無い事は行いません。
もし、設計者が、ステップ数や、実行時間の比率などを考えて、関数を展開した方が有益と判断されるなら、「inline」を宣言する事で、その関数は展開され、呼び出しのコストが取り除かれます、ただ、注意しなければならない事があります、どんな場合でも「展開」する事が有益だとは限らない事です、たとえばキャッシュが小さいマイコンの場合は、展開する事で、キャッシュのヒット率が悪くなり、逆に速度が落ちるかもしれません、結局、計測してみないと本当の事は判らないと言う事です。
※もちろん、キャッシュが無いような小規模なマイコンでは、当てはまりません。
※「計測」は非常にスキルの必要なエンジニアリングです、浅はかな計測では大抵真実は明らかになりません。

組み込みで、良く使われる define の利用法として、ハードウェアーの制御や定数の定義などがあります。

たとえば、ボーレートの初期値として・・・

#define BAUD_RATE 9600

これは、単純に

static const int BAUD_RATE = 9600;
又は、
enum rate {
  BAUD = 9600,
};

のように、名前空間などを利用した構造的な定義が出来ます。
最適化されれば、余分なメモリーを消費する事も無く、参照されずに直接アセンブラ命令に埋め込まれます。
※この場合の定数は、「int」型となりますので、int 型が都合悪ければキャストする必要があります。

良くありがちな I/O デバイスの操作で・・

void write_data(u8 data) {
  ACTIVE_PORT;    // ポートを有効
  OUT_DATA(data); // データ出力
  WRITE;          // ライト
  CS_LOW;         // CS 有効
  SETUP_DELAY;    // データのセットアップタイム
  CS_HIGH;        // CS 無効
  INACTIVE_PORT;  // ポートを無効
}

↑のように、あるデバイスにデータを出力する手続きを実装したものです。
※本来、大文字で書かれた命令は、普通に関数(inline 関数など)を定義しても良いのですが、内部で行っている事が、少なく、関数を定義するのが「冗長」又は、マクロ展開で処理コストを節約とかの判断なのでしょう、典型的な C のプログラマーは、これを define で定義する事が多いように思います。
※また、define では、パラメーターを埋め込んで、適当に整形して展開出来る為、かなり複雑な事も可能ですが、C++ では殆どの場合、テンプレート関数でそれを置き換える事が出来ると思います。

#define ACTIVE_PORT { ... }
これは、単純に・・
inline void ACTIVE_PORT() { ... }

#define OUT_DATA(d) { ... }
引数がある場合は、その引数の「型」を定義できるので、正確で安全です。
inline void OUT_DATA(uint8_t d) { ... } 

 
次の例では、一次元配列に対して、二次元配列的にアクセスするマクロです、型に依存しない様にテンプレートで表現しています、define を使わなくても可能な例です。
※配列サイズを超えたら、例外を投げるとか、色々拡張する事もできるでしょう。
※参照を使っているので、関数に直接値を代入するような書き方が可能です。

/// 読み出し専用
template <typename T>
const T& get_dim_(const T* p, uint32_t row, uint32_t col) {
    return p[(col << 2) + row];
}

/// 書き込み専用
template <typename T>
T& put_dim_(T* p, uint32_t row, uint32_t col) {
    return p[(col << 2) + row];
}
-----
    float mat[16];
    
    put_dim_(mat, 1, 3) = 10.0f;

    float a = get_dim_(mat, 1, 3);

 
 
(6)キャスト
C では、「キャスト」について、アセンブラ的に、単純に警告を取り除く為の手段として利用しているのが殆どのように思います。
C のキャストはたとえば、こんな感じの物です。

  char ch;
  ...
  unsigned char data = (unsigned char)ch;

C++ では、いくつかのキャストが用意されており、場合によって使い分けます。
  unsigned char data = static_cast<unsigned char>(ch);

良く、C++ のプログラムで、C のキャストを使う人がいますが、C のキャストは、変換が可能か不可能かに関係なくエラー検査無しに変換します。
※驚く事に、「C++ のキャストは冗長だから、C のキャストを使う」と言っている人がいましたが、反面教師にすべきです。
C++ では、static_cast を良く使いますが、もし変換が出来ない場合はエラーとなります。
C++ のプログラムでは、C のキャストを使わないようにして下さい。
また、const を取り除くようなキャストも行ってはなりません、そのような操作は、基本的に間違っています、通常、設計を正しくする事で避ける事が出来ます。
※ C++ の他のキャストについては、リファレンスを参照して下さい。
 
 
(7)警告
コンパイラが出力するレポートは非常に重要です、エラー検査は最大レベル(厳しいエラー検査)を設定し、一つでも警告が出たら、実行する前に、警告を取り除くようにソースコード修正する「クセ」をつける必要があると思います。
良くありがちな事として、「警告は最後にまとめて取り除く」という方針を実践している人がいますが、間違いです、「警告」が出るのには理由があり、コンパイラがソースコードの構造的欠陥を指摘しているのですから、警告が出ないような、抜本的な改修や、綺麗な設計が必要です、これらの優先順位は最後に回すべきものではなく、「今」行うべき事項であると認識して下さい。
※とりあえず「警告」を無視する事の無いように・・・
※「警告」が沢山出るプログラムは、そもそも設計が良くない場合も考えられます。