std::map型を使って見通しのよいコーディングをしよう


 ある程度のプログラムを書いていくと、プログラムはたちまち行数が増えていって全体像の見通しが悪くなりがちです。enum型で表している項目が改版のたびに項目が追加されていくことが起こりがちです。項目が追加されるにしたがって、switch文やif else if文がどんどん増えていって行数がディスプレイに一度に表示できないようになっていきます。
 そのようなコーディングスタイルは、STL(=standard template library)のコンテナのデータ構造を使う実装に置き換えましょう。そうすると、見通しのよいソースコードになります。
 そのような状況にある人は、このページを使って開発チームの他のメンバーにも、楽しいC++生活を勧めて下さい。
 以下の文章の中では「STLを使うのは常識だよ」という人に役立つ知見は何もありません。

 enum型を使えば十分なのに、マクロ定数を使っているようなソースコードの場合、std::map型などのデータ構造を使っていることはないでしょう。enum型を使っている変数の場合、そのenum型に応じた動作をするようにswitch文や if else if else if elseなどの文が書かれていることが多くなります。しかも、そのenumの要素が多くなってくると見通しが悪くなっていきます。

STLのコンテナを使う前のコードの例

withoutMap.cpp
#include <map>
#include <string.h>

/**
* @元素記号に対応する原子番号を返す
* @param[in] 元素記号を示す文字列
*/
int getAtomicNo(char* instr){
    int r = 0;
    if (strcmp(instr, "H") == 0){
        r = 1;
    }else if (strcmp(instr, "He") == 0){
        r = 2;
    }else if (strcmp(instr, "Li") == 0){
        r = 3;
    }else if (strcmp(instr, "Be") == 0){
        r = 4;
    }else if (strcmp(instr, "B") == 0){
        r = 5;
    }else if (strcmp(instr, "C") == 0){
        r = 6;
    }else if (strcmp(instr, "N") == 0){
        r = 7;
    }else if (strcmp(instr, "O") == 0){
        r = 8;
    }else if (strcmp(instr, "F") == 0){
        r = 9;
    }else if (strcmp(instr, "Ne") == 0){
        r = 10;
    }
    return r;

}

int main(int argc, char* argv[]){
    //"Be"の原子番号を求めるプログラム
    char instr[] = "Be";
    int r=getAtomicNo(instr);
    printf("%s の原子番号 %d \n", instr, r);
}

実行結果
>Be の原子番号 4

STLのmap型(std::map)を使った書き換え例

withMap.cpp
#include <iostream>
#include <string>
#include <map>

/** 元素記号から原子番号を対応させるmap型のコンテナ
* 宣言と同時に初期化を実施。
* (第2周期のNeまでしか記載していない。)
*/
std::map<std::string, int> atmomicNumbers = {
    { "H", 1 },
    { "He", 2 },
    { "Li", 3 },
    { "Be", 4 },
    { "B", 5 },
    { "C", 6 },
    { "N", 7 },
    { "O", 8 },
    { "F", 9 },
    { "Ne", 10 }
};

int main(int argc, char* argv[]){

    std::cout << "Be" << " " << atmomicNumbers["Be"] << std::endl;
}

はたして、どちらのコードをメンテナンスしたいと思いますか?

STLを使う場合の利点

・必要なデータ構造がまとまって記載される。
 ->データが正しく記述されているかのチェックが楽になる。
 ->データ項目が追加になったときに、修正する箇所が1箇所になる。
・ロジックとデータとが分離される
 この例では、元素記号から原子番号を求めるというロジックの部分は、元素記号と原子番号のペアが増えてもなんら影響を受けない。
・STLのデータ構造に標準で備わっているメソッドを使うことでプログラムのロジックが短くなる。
 ->プログラムが簡潔になる。
 ->プログラムのロジックにバグが入りにくくなる。
・豊富なメソッドが標準化されている。
 -> 誰が書いても似たようなコードになる。
 -> 他の人が書いたコードを理解するのにかかる時間が節約される。
・自作ライブラリで生じがちなエラーは生じにくくるなる。
 ->とりあえず100個の配列を用意してあるから大丈夫だろうというコーディングはなくなる。

STLを使う場合のデメリット

・移植対象の処理系でC++をサポートしているかを確認する必要がある。
 ->RaspberryPi のg++ でも十分にSTLはサポートされているようだ。
・std::mapを使っても、コンパイル後の大きさ、実行速度で問題を生じないことを示す必要がある。
  ->Visual C++ 6の時代のような大昔のコンパイラだと、意図した動作をしないことがあった(C++自体にしても)
・開発チームの他のメンバーにとってもわかりやすいコーディングになるように配慮すること。

まとめ

 enum型を使って何かを処理している場合には、enum型の要素をキーとして、処理に応じた値を戻り値にするstd::map型のデータ構造の方が記述が簡潔になる。
 STLのコンテナを使うと、ライブラリのロジックとデータとが分離されるようになるので、今後データが追加されることがあっても、ライブラリのロジックは完成されて改変が不要になる。

注意:
 この例題の中では、mapのキーに含まれていない値が入力されたときのことを考えていません。

関数ポインタの利用

 std::map型を使って記述する際に、map型の戻り値を関数へのポインタにすることもできる。std::map<std::string , double(*)(double, double)>というようにしてみた。
そうすると、funcMap[letter](a, b)という簡潔な記述で、if文やswitch文を使わずに"+", "-", "*", "/"のどれかに応じて、対応する関数の呼び出しをできる。
 その分、動的な要素が増えるので、次の例では mapのキーに含まれていない値が入力されたときのことを考慮するコードとした。

funcPointer.cpp
#include <iostream>
#include <map>
#include <string>

double Add(double a, double b){ return a + b; }
double Sub(double a, double b){ return a - b; }
double Mul(double a, double b){ return a * b; }
double Div(double a, double b){ return a / b; }

std::map<std::string , double(*)(double, double)> funcMap= {
    { "+", Add },
    { "-", Sub },
    { "*", Mul },
    { "/", Div }
};

int main(int argc, char* argv[]){
    while(true){
        std::string letter;
        std::cout << "演算子を指定して下さい > " << std::flush;
        std::cin >> letter;

        if (funcMap.find(letter) != funcMap.end()){
            double     a, b;
            std::cout << "2つの数値を入力して下さい > " << std::flush;
            std::cin >> a >> b;
            std::cout << a << letter << b << " = ";
            std::cout << funcMap[letter](a, b) << std::endl;
        }
        else{
            std::cout << "そのような演算子はありません" << std::endl;
        }
    }
    return 0;
}

付記:入力がmap型のkeyに含まれていない可能性あるときのコードの書き方

 ブログ記事findを使用した検索 
 map型へのfind()メソッドを使ったやりかたで対応できます。switch文のdefault節の動作に対応する記述が書けます。

付記:オープン・クローズドの原則:

モジュールは「拡張に対して開いていて、変更に対して閉じていなければならない」ということに対して、std::map型を使った設計はみたしやすい。switch文で対応するやり方だと、オープン・クローズドの原則に対応しきれない場合があって、抽象化が必要なことを述べているページを見つけた。
 [オブジェクト指向設計原則]オープン・クローズドの原則(OCP)

関連記事
設計の確定しきらない部分をstd::unordered_map型に押し込もう

快適なC++生活のためにスクリプト言語を使おう2 C++のソースを自動生成させる
を書いてみました。std::map型のデータから、if else if文を自動生成させるスクリプトの実例を示しました。