2018-01-06

Cプログラマのための、C++簡単裏道アプローチ(6)

いつも同じようなデータの入れ物を作っているならSTLが使える

裏口アプローチの最後は、データの入れ物(コンテナ)の話です。

今回も、既にC++を知っている人がCを使わざるを得ない環境で、
「ああC++のこの機能だけでも使えたらな」
と、感じるような内容です。

配列はC言語にももちろん備わっていますが、固定長です。可変長の配列が必要な場合、realloc()等での操作が必要になるなど、かなり複雑で面倒なことになってしまいます。

まずはC++での可変長配列の例を見てみましょう。

コード1

void func1()
{
    vector<int> numbers = {1,2,3,4};  // 配列の宣言と初期化

    // 要素1を200に変更->{1,200,3,4}
    numbers[1] = 200;

    // 配列の末尾に要素(10)を1つ追加->{1,200,3,4,10}
    numbers.push_back(10);

    // 要素2の前に300を挿入->{1,200,300,3,4,10}
    numbers.insert(numbers.begin() + 2,300);

    // 要素4を削除->{1,200,300,3,10)}
    numbers.erase(numbers.begin() + 4);

    // 表示
    for(int i = 0 ; i < numbers.size() ; ++ i)
    {
        printf("numbers[%d]=%d\n", i, numbers[i]);
    }
}
デモンストレーション的に無意味な操作をしていますが、末尾に要素を追加するのはもちろん、途中への挿入、削除なども行っています。
これらと同じ処理をCで行うことを想像してみれば、比較にならないほど圧倒的であることが容易に判断できますね。

もう少し、このコードについて説明します。

関数の最初で vector<int>という型でnumbersという変数(オブジェクト)を宣言し、通常の配列のように初期化しています。ここで使用している<int>の部分を変更すれば、要素にあらゆる型を適用することができます。

コード2

struct XY {int x; int y;};

vector<int>     intNums = {1,2,3};
vector<double>  doubleNums = {1.0, 2.0, 253.9991};
vector<XY>      xys = {{1,100}, {2,300}};
特に注目して欲しいのは、3番目の独自の型を適用できる点です。

まず、最初に説明しなければならないのは、ここで使用しているvectorというのは単にC++のライブラリであるということです。まるでC++という言語に組み込まれているかのように見えるところもありますが、STLというライブラリの1つです。

STLは標準テンプレートライブラリ、という名称です。テンプレートはC++の文法の1つで、型そのものをパラメータとしてコーディングすることができる機能です。

テンプレートを関数に適用した例を示します。max()はCではマクロとして実装できますが、マクロには副作用があるため、通常の関数の方が望ましいわけです。しかし、そのまま関数にしてしまうと仮引数の型指定によって1つの型に決まってしまい、それ以外の型に対応できなくなります。そこでテンプレートを使用します。

コード3

// 関数の実装
template <typename T>
T max(T var1 , T var2)
{
    if( var1 > var2 )
        return var1;
    else
        return var2;
}

// 関数の使用例1
void func2()
{
    int    iMax = max<int>( 111, 112 );     // iMax = 112
    double dMax = max<double>( 1.2, 1.1 );  // dMax = 1.2
}

// 関数の使用例2
void func3()
{
    int    iMax = max( 111, 112 );  // iMax = 112
    double dMax = max( 1.2, 1.1 );  // dMax = 1.2
}
これはテンプレート関数と言いますが、関数の使用例1の基本的な記法を見ると、<typename T>というタイプパラメータにintやdoubleが適用されることが分かりやすいと思います。
使用例2は<int>等の部分を省略した記法で、この場合maxの引数部分の型から自動的に判断されます。

テンプレート機能を使用した場合、その実装部分ではまだ実際には仮の状態であり、使用部分で型が決まった時点で実装が完結する仕組みになっています。
余談ですが、そのためライブラリと言ってもその一部、または全てがソースコードで提供されることになります。


さて、ここまでは比較的シンプルな配列という入れ物を見てきました。C++ライブラリの柔軟性やその可能性について感じていただきたかったからです。
しかし、データの入れ物として、単なる配列だけではその有用性は限られたものに感じられるかも知れません。STLには他にも次のような種類のコンテナが用意されています。

  • vector:動的な配列
  • map:キーと値のペアを格納する(キーはユニーク)
  • multimap:キーと値のペアを格納する(キーは重複可能)
  • set:値を持たないmapのようなもの(ユニーク)
  • multiset:multimapの値を持たないようなもの(重複可能)
  • list:双方向リスト
  • forward_list:片方向リスト
  • deque:両端キュー
  • queue:キュー
  • priority_queue:優先度付きのキュー
  • stack:スタック

以上は代表的なもので、他にも多少バリエーションが存在します。また、以前紹介したstringも“コンテナ相当”と位置づけられていて、コンテナ相当のものは他にもいくつか存在します。

キューやリスト、スタック等はなじみ深いと感じる方も多いかもしれません。毎回、新しい開発の度に少しずつ違う構造体を扱うために同じ機能の似たようなプログラムをよく作っている、という人も多いのではないでしょうか。現実的に、C言語ではそうせざるを得ません。しかし、C++を知っているとそういった労力が無駄に思えてきます。

もう1つだけ、上記の中からmapを紹介しておきます。これは連想配列を実現するものなのですが、メモリ上に簡易データベースを構築できると考えてもいいものです。ただし、本格的なデータベースに比べれば機能も限定的ですし、パフォーマンスも期待できません。
とは言え、連想配列はその使用方法が非常に明快かつ柔軟性もあり、初めて知ったときは“目から鱗”的な感慨があったと記憶しています。

コード4

void func4()
{
    map<const char *,double> populations;

    populations["東京都"] = 927.3; // 東京都は927.3万人
    populations["横浜市"] = 372.5; // 横浜市は372.5万人
    populations["北海道"] = 547.4; // 北海道は547.4万人

    printf("横浜市の人口は%f万人\n", populations["横浜市"] );
}
これは機能の説明はほとんど要らないのではないでしょうか。一目瞭然の動作ですね。

配列の添え字に数値ではなく、文字列を使用しています。そして、元々なにもないところに値を設定すると、その項目が追加されます。また、既に項目が存在すれば、データは新しいものに置き換えられます。

もし、項目がヒットしなければ(見つからなければ)値のデフォルト値(この場合doubleなので0.0)が返されます。ヒットしないことを知りたいなら[]による添え字ではなく、組込みのfind()関数を使用すればいいだけです。

添え字を使った場合、項目がヒットしないとデフォルト値が返されることには意味があります。次のコードを見てください。

コード5

void func5(const char * name)
{
    static map<const char *,long> nameCounts;

    printf("%sは%ld人\n", name, ++ nameCounts[name] );
}
nameが初めて出現した場合、longの初期値0が返されるので、インクリメントは期待通り1からカウントされます。

最後に


ここまで拙い記事を読んで頂き、ありがとうございました。

今はすっかりオブジェクト指向がネイティブである世代が活躍、台頭している時代と思っていましたが、最近のある経験で案外そうではない人たちも大勢いる、ということが判ってきました。おそらく、彼らは同じ環境にずっといて、彼らにとって新しい世界を知るきっかけを奪われていたのだろうと思われます。(インターネットの規制のある国の人たちだって海外旅行は自由にできるのに、自分たちの国の体制に疑問を持つ人がほとんどいないのと似ている気がしました)

最初は信じられませんでした。それ、冗談だろう? と思うようなことがあって、自分にとって30年位前に終わっていた議論を、彼らは未だにしていたのです。どうでもいいことや、明らかに世の中が結論を出していること、あるいは論理的に結論していることをほじくり返していたのです。他に考えることがないのかも知れません。当然ながら、彼らの書くコードは洗練とはかけ離れたものに見えました。

オブジェクト指向を研究すれば、もっと大切で肝となるような議題はいくらでもあります。そして、それはオブジェクト指向設計の醍醐味の1つでもあります。

しかし、オブジェクト指向というのは、何も知らない人にとっては大きな壁に見える可能性があります。だから、今回はあえてオブジェクト指向をできるだけ避けて、それでもC++によるプログラミングの素晴らしさの一部を知っていただきたく、記事を書きました。C++という言語のパワーの一端をご紹介したつもりです。

しかしながら、この言語がさらに強力に威力を発揮するのは、やはりオブジェクト指向設計を行ったときです。これまでご紹介してきた内容は、それに比べればおまけ程度のものなのです。

いずれ、機会を設けてオブジェクト指向プログラミングの裏口入門の記事を書いてみたいと思っています。オブジェクト指向の聖書?のように荒唐無稽でつかみ所のない宗教画、あるいは絵に描いた餅を見ながら学ぶのではなく、目の前にある問題を解決する簡単な応用例を示し、必然性を感じて頂きながら進めていけるものにしたいと思っています。

それでは、またいずれ。

0 件のコメント:

コメントを投稿