2月30日

役立つかもしれない情報をノートとして残しておく。

コーディング規約

例えばC++でプログラミングをしてコンパイルをすると実行ファイルができますが、これは「C++という人間が比較的理解しやすいプログラミング言語を使ってコンピューターに自分が何をしてほしいかをソースコードに書き、それをコンパイラにコンピューターが理解できる形式にしてもらう」ということをしています。コンパイラにとっては、C++ソースコード中に含まれるクラス名や関数名が何であろうと関係ないので、コーディングする人はC++文法を遵守する限り、どんな書き方をしてもコンパイラ的には問題ありません。

ただし、仕事が進むと複数名で1つの大きなプログラムを書くことが増えますし、自分が別の仕事に移った後、共同開発者などがそのプログラムを改良したりメンテナンスすることもあります。このような場合、プログラミング言語の文法的にミスのないソースコードを書くだけでは不十分で、「自分が書いたソースコードを他の開発者が読むことで、自分がコンピューターに何をしてほしくてそのコードを書いたのかを理解できる」ということも重要になります*1。そのために、コーディング規約を定めておくことが有用です。

以下に私が普段倣っているコーディング規約を、一例としてまとめてみます*2。コーディング規約は必ずしも厳密なルールではなく、一般的に使われている規約にもいくつかパターンがあります。開発チームの中で、自分が普段使っているものと異なるコーディング規約が使われている場合には、チーム内で使われている規約を優先するべきです。また、時と場合によっては取り決めから外れた書き方をした方がわかりやすくなることもありますし、1秒でも早くデータ解析結果を出さなければならない時にはコーディング規約なんて守ってられないこともあると思います。

変数やポインタなどの命名規則

適切かつ簡潔な英語に基づいて、意味のある名前を付ける。

  • 良い例:int lineNumber = 0;
  • 悪い例:int nl = 0; // 他の開発者はnlが何を意味するのか分からない。
  • float valueとかint numberのような変数名は抽象的で、何のvalueやnumberなのかわからない。例えば電流の測定値だったらfloat measuredCurrent、シリアル番号だったらint serialNumberのように具体的に意味が分かるように名前を付ける。
  • 省略形は、共同開発者の中で共通認識となっているもののみ使用可。例えばASICとかFPGAは比較的一般に知られている言葉なので使ってよい。例えばdata analyserのような自作パッケージを書いたとして、それを呼ぶときにDAと略すと何のことかわからないので、dataAnalyserと書いた方が無難。

似たような名前の変数を定義しない。

  • 例1:taggerTagger; // 1文字目が大文字か小文字かの違いしかない
  • 例2:SOとS0; // 1つ目はエスオー、2つ目はエスゼロ

privateまたはprotectedなメンバー変数はm_からスタートする。それ以外の変数は絶対にm_から初めてはいけない*3

  • 例:int m_serialNumber;

class、enum、typedefの名前は大文字から始める。変数や関数名は小文字から始める。

  • 例:class Tagger;
  • 例:enum Color {green, yellow, red};
  • 例:typedef vector<DataAnalyser*> InputVector;
  • 例:double energy;
  • 例:void extrapolate();

2単語以上の場合、1単語目の大文字 or 小文字は上記に倣い、2単語目以降は単語の1文字目を大文字にしてつなぐ。(キャメルケースと呼ぶ)

  • 例:class DataAnalyser;
  • 例:double measuredCurrentVsAppliedVoltage;

ブーリアン(bool型)を定義するときには、直感的なわかりやすさに気をつける。

  • 良い例:bool isOddNumber = true; // これなら「奇数ならtrue」と直感的にわかりやすい。
  • 悪い例:bool isNotOddNumber = true; // これだと「奇数でないときにtrue」と否定と肯定が混ざり合って混乱を招く。例えば、if (isNotOddNumber) とするより、if (!isOddNumber) と「isOddNumberが偽のときに実行せよ」という書き方をした方が可読性も高い。

bool型変数の変数名は、isやその他一般動詞から始めるようにすると意味がはっきりしてわかりやすいことが多い。

  • 良い例:bool isOddNumber = true;
  • 悪い例:bool oddNumber = true; // odd numberというのは「奇数」を表す一般的な名称でしかないので、あとからコード中にoddNumberが出てきたときに、これがbool型だということがすぐにはわからない。

コードの書き方の作法

コードの書き方変数は実際に使う場所の直前で定義し、必ず初期化する。

  • 良い例:

    int channel = 0;

    if (setChannel) channel = atoi(argv[1]);

    measureCurrent(channel);   

  • 悪い例:

    int channel;

    if (setChannel) channel = atoi(argv[1]);

    measureCurrent(channel); // もしsetChannelがfalseになると、channelが初期化されずにmeasureCurrent関数に渡されてしまい、挙動がおかしくなったりセグバイを引き起こしたりすることがある。

マジックナンバーリテラル(要するに、値の直書き)はできる限り避ける。

  • 良い例:      

    class HistogramHandler {

        ...

        TH1F *m_histogram;

        static TString m_histPrefix;

     }

     ...

     m_histPrefix = "hist_";

     ...

     void HistogramHander::getHistogram(TFile *fData, int channel) {

       m_histogram = (TH1F*)fData->Get(m_histPrefix + TString::Itoa(channel, 10));

     }

  • 悪い例:

    class HistogramHandler {

        ...

        TH1F *m_histogram;

    }

    void HistogramHander::getHistogram(TFile *fData, int channel) {

        m_histogram = (TH1F*)fData->Get(TString("hist_") + TSTring::Itoa(channel, 10));

    } 

 

やってしまいがちなのは、解析したいチャンネル番号や動かしたい装置のアドレやポート番号などをソースコードの中に逐一直書きしてしまうこと。そうすると、それらの値を変更する必要が生じたとき、どこを合計何箇所変更すべきかがわからなくなり、全部直したつもりが一部直っていなかったためにエラーになったり、最悪の場合自分が気づかないうちに誤ったチャンネルのデータを解析してしまうこともある。

その場しのぎのインデックスを定義せず、enumを適切に使う。

  • 良い例:

    enum MeasurementType = {resistance, capacitance, current};

    int const numberOfMeasurements = MeasurementType::current + 1;

    float results[numberOfMeasurements] = {0};

    ...

    results[Measurement::registance] = getResistance();

    results[Measurement::capacitance]  = getCapacitance();

    results[Measurement::current]  = getCurrent();

    ...

    std::cout << results[Measurement::resistance] << std::endl; // この行を見るだけで抵抗値の測定結果を表示していることがすぐわかる。  

  • 悪い例:

    float results[3] = {0};

    ...

    results[0] = getRegistance();

    results[1] = getCapacitance();

    results[2] = getCurrent();

    ...

    std::cout << results[2] << std::endl;

この場合、[0] = 抵抗値、[1] = 静電容量、[2] = 電流と対応づけられていることが、results[i]に値を詰めている部分を読んで初めてわかるが、この関係は第三者にとって自明でない。特に、std::cout行が離れていると、results[2]が結局何を出力しているのかという意味がわからない。

コメントを適切に入れる。

特に複雑な処理をしている部分は、コメント無しで他の開発者が理解することは難しい。コメントはできるだけ、自分以外の人に説明するつもりで丁寧に入れる。

余分なコメントアウトは残さないで削除する。

試行錯誤をしているうちはコメントアウトを残しておいた方がよいこともあるが、一通りテストが終わったらコメントアウト行は「念のため」で残さず削除した方が良い。もし、何らかの理由でデバッグ用に残したいときには、コメントアウトで対応せず、デバッグフラグを設けておいて、それがtrueの時にはその行を実行する、というようにする。

デバッグメッセージの出力方法

デバッグ中、ある変数にどんな値が入っているかをstd::coutして表示したくなることがある。その値が出力結果を見てどんな意味を持つか自明なうちはいいが、あまりに何でも出力すると、結局その値がどこで出力されているのか、第三者にはわからなくなる。デバッグが終わった後、残しておいた方が有用な表示は、それが何を意味するのかわかるようにしておく。不要な表示は残さず削除する。

たとえば、以下の場合

    for (int ix = 0; ix<10; ix++) {
        float x = ix * 0.1;
        std::cout << x << std::endl;
        float x_shifted += 0.2:
        std::cout << x_shifted << std::endl;
    }

表示結果は以下のようになる。

    0
    0.2
    0.1
    0.3
    ...

これを100行表示した後、どちらがxでどちらがx_shiftedだったかわかるだろうか。これぐらいであれば規則性があるのでわからないこともないが、例えば変換がより複雑だったり、これに例外処理が入ってx_shiftedが表示されないことがあったり、処理が複雑になるともはや第三者には追うことができなくなる。

    for (int ix = 0; ix<10; ix++) {
        float x = ix * 0.1;
        float x_shifted += 0.2:
        std::cout << "(x, x_shifted) = (" << x << ", " << x_shifted << ")" << std::endl;
    }

 これであれば、(x, x_shifted) = (0, 0.2)のように、意味も合わせて表示されるため、第三者でも一目で値の意味が分かる。

デバッグしながらコードを書いてるときは頭がフル回転しているためこういう体裁的なことまで気が回らないことが多いかもしれないが、最後人に渡すとき(例えば人に使ってもらうとか、gitにマージするとか)には、10分だけ時間を割いてきちんとわかりやすく整理し、不要な行を削除しておくだけで、他人の数時間を節約することにもなる。

 

*1:自分専用プログラムでも、真面目にわかりやすく書いた方が、後からコードを見返す必要が出た時に圧倒的に楽です。

*2:某実験グループ内部で規定されているコーディング規約に基づいています。基本的にはC++でコーディングする際の標準的な規約が踏襲されているため大いに参考になりますが、細かい部分については、単に実験グループないではこのような表記が推奨されているというだけの部分もあります。

*3:これ、ちゃんと区別して書かないと、本当に他人には挙動がわからないプログラムになります。ローカル変数にm_を付けるのは問題外として、グローバル変数に何も接頭辞を付けず、関数の中で似たような名前のグローバル変数とローカル変数を混在させてしまうというのは非常にありがちです。