ぷろみん

プログラミング的な内容を扱ってます

C++コーディングガイドライン哲学1

概要

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md
の「P: Philosophy」の感想や補足です。

アイディアを直接コードで表現する

例としてStrong typeやfor文の代わりにアルゴリズムを使うことを提案しています。
また、constを一貫して使うことやキャストによる型システム破壊の防止について言及しています。

Strong type

プリミティブ型のような汎用性の高い型は間違いの元なので、間違った操作ができない型を使おうという発想です。

IndexA indexA(10);
IndexB indexB(5);
A a;
B b;
a[indexA]; // OK
a[indexB]; // コンパイルエラー
if(indexA == IndexA(10) && indexB == IndexB(5){} // OK
if(indexA == 5 && indexB == 10){} // コンパイルエラー

Indexが全部int型だと必ずミスをします。自分は良くても後で保守する人は必ずミスをします。

スタンダードライブラリにある機能を再発明しない

これは簡単なようでいてかなり難しいです。これを正しく運用するためにはスタンダードライブラリの全機能を把握する必要があるからです。無理です。
なので、よくある処理を書く前にライブラリにないかな?といったんググるだけで良いと思います。
良く使いそうな例を少し挙げておきます。

一般的にあって欲しいfilterとmap(LinqだとWhereとSelect)もあるといえばあるけど・・・状態なので結局作らなければいけないことも多いです。
boostまで考慮に入れるともう少し便利になります。

  • std::remove_ifとcontainer::erase
    条件を満たした要素の配列を取得できます。
    参照配列を弄って欲しくない時はコピーするしかない?

  • std::transform 指定した方法で変換された配列を取得します。
    変換後結果を詰めた配列を返すものということは知っておいた方が良いでしょう。 Boost.Rangeをパイプライン記法のままコンテナに変換 - Qiita

for文でこの処理を書くとかなり蛇足感が出ます。

  • std::all_of 全ての要素が条件を満たすか確認します。

  • std::any_of どれか1つでも条件を満たす要素があるか確認します。

algorithmぐらい頻繁に使うがnumericにあるので存在を忘れやすい

  • std::accumulate 総和を求める

constを一貫して使う

constは伝搬されるものなので最初から心がけていないと「あれ?この関数にconst変数渡せないぞ」となってしまい、constを使わない文化が蔓延してしまうので早めに取り組んだ方が良いでしょう。
constに気をつける箇所はたくさんあります。

class Input {
public:
  // メンバ関数のconst
  Arguments GetArguments() const {
    return arguments;
  }

private:
...
  // メンバ変数のconst
  // コンストラクタで初期化しないとエラーとなるので使わない人も多いですが、const性を持つのなら定義すべきです
  const int version;
};

// 引数のconst
void Function(const Input& input) {
  // ローカル変数のconst
  // 蛇足っぽく見えるので嫌う人も多いですがconstを常に使う方が哲学に沿います
  const auto& arguments = input.GetArguments();
}

キャストした際にはそれがキャストされた値だと明示する

おそらくstd::anyやstd::variantについて言及しているのだと思います。
別の型として汎用管理しても良いけど元の型を忘れちゃダメ。

ユーザ定義リテラル

例として下記のようなコードが提示されています。

change_speed(23m / 10s);  // meters per second

何の説明もなく出現していますが、これは疑似コードではなくユーザ定義リテラルです。
ユーザー定義リテラル - cpprefjp C++日本語リファレンス

なにかと評判の良くないユーザ定義リテラルですが、ガイドラインで利用されていることを考えると使っていった方が良いのでしょうね。

ISO Standard C++を使う

理由は移植性だと思うのですが、このガイドラインはISOのために書かれているからISO Standard C++を使おうってなってます。
移植性では納得しない人が多かったのでしょうか。
まー、守ることは環境依存な拡張機能は使わないようにしようってことです。

意図を表現しよう

例えば配列の各要素について処理したい場合はwhileではなくforを使おうということです。
上記ぐらいは多くのプログラマーが実施できているとは思いますが、Read Writeの意図を明示できている人は思っているよりも少ないです。

// 参照のみすることを明示する
for (const auto& x : v) {}
// 書き換えを行うことを明示する
for (auto& x : v) {}

また、プリミティブ型は曖昧になりがちなので意味を表現した型を使うことを勧めています。

// 各パラメータが何を指しているのか分かり辛い
draw_line(int, int, int, int);
// 分かりやすい
draw_line(Point, Point);

最後により良い互換機能が提供されているものについてはそちらを使いましょう。

  • 単純なforはrage baseのforに
  • ポインタとその長さの組み合わせによる範囲指定はspanに spanとはGuideline Support Library(gsl)に定義されるクラス。MSによる実装が下記
    GSL/span at master · Microsoft/GSL · GitHub
  • あまりに長いスコープ変数 ネストが深くなったりスコープが長くなり過ぎる場合、意味単位に分割してクラス化や関数化を行い、スコープはせめて1画面以内にするのが望ましいと私は思います。
  • 生のnewやdeleteはスマートポインタやmake_shared、make_uniqueに
  • 関数の引数にプリミティブ型が多い場合は意味を表現した型を使うように

理想的には、プログラムは静的型安全であるべき

クラッシュやセキュリティの脅威となる既存の方法への代替案として下記を提案しています。

  • union(共用体)の代わりにstd::variant
  • キャストの代わりにtemplate
  • 部分配列の利用にポインタやインデックスを使うのではなくgsl::span
  • 範囲外アクセスエラーもgsl::spanなら検出できる
  • キャストするとしてもgsl::narrowやgsl::narrow_castを使おう narrowはnarrow<IndexA, IndexB>でIndexAをIndexBにstatic_castした値が入っているよと明示するクラス

実行時チェックよりもコンパイル時チェックの方が良い

間違ったコードはコンパイル時にエラーを出し、決して実行時までエラーを遅延させてはいけない。
また、static_assert等で検証した事項を実行時に再検証してはいけない。

コンパイル時にチェックできないものは、実行時にチェックできなければならない

上記タイトルだと幅広そうですけど、ようするにレンジアクセスにポインタとインデックスは使わずにgsl::span使おうぜって内容です。

実行時エラーは早めに捕捉しよう

nullポインターに代表される多重チェックをしないためにどうするべきか。

nullチェックが必要なポインタの代わりに参照を使うように、境界値チェックが必要な配列とレンジを指定する代わりにspanを使うようにチェックが終わって安心して使えることを明示する型を利用するようにしましょう。ということだと思います。次の項目で出てくるRAII的な考え方をエラーチェックにも適用しようということでしょう。

おわりに

これで哲学の項目の半分くらいです。続きは気が向いたら書きます。