2018-01-14

Cプログラマのための、
C++オブジェクト指向簡単裏口入門(5)

プライバシー問題!?

とてもソフトウエア工学とは直接関係なさそうなタイトルですね。でも、実際にはプログラミングにとって非常に重要な考え方で、是非、これは覚えるだけではなく、身につけて今後に活かしていただきたいと願います。
特に、C言語だけを何の問題も感じずに長年やってきた人にとっては、ひょっとすると面倒臭い余計なものに感じられるかも知れませんので、注意が必要です。

実社会のプライバシー問題

実社会でも、以前の日本ではプライバシー問題という言葉すらほとんど聞くことはありませんでした。
ところが、時代が新しくなるにつれ、人間関係が疎になっていったし、習慣の違ういろんな人が集まるようになってくると、赤の他人に必要以上の個人情報を知られて悪用されることに、社会全体が敏感になっていったのです。

プログラミングのプライバシー問題?

プログラミングも昔はプライバシー問題に鈍感でした。パーソナルコンピュータが一般に普及し始めた頃、おまけで付いていたインタプリタ言語(N88BASIC)にはグローバル変数しかありませんでした。

しかし、多機能、高機能が求められ、プログラムが複雑、かつ大きくなってくると限界が見え始めます。そんなときに構造化プログラミングの必要性が浸透すると共に、C言語が普及し始めました。関数という単位でプログラミングをし、関数の中だけで使える変数がある。それだけでもプログラマにとってとてもありがたく、助かりました。

オブジェクトのプライバシー

ハードウェアの進化と共に、ソフトウエアに求められる機能はより多く、より高度になっていきました。当然プログラムは複雑、かつ巨大にならざるを得ません。

この問題解決の一助となったのがオブジェクト指向プログラミングです。

これまで、プログラミングにおけるオブジェクトとは、ただのデータ構造でしかありませんでした。関数などの処理コードがまずあって、そのコードが操作する対象がデータ構造でした。
オブジェクト指向ではこのデータ構造を主体に考えます。そして、コードはデータ構造に従属するという考え方に変わります。つまり、データ構造とコードが非常に密接に関連付けられることになったのです。

その、オブジェクト指向で言うところのオブジェクト、つまり、そのデータ構造に従属する専用の処理コードもひっくるめてのオブジェクトは、これまでの関数という単位よりも高い独立性を確保することができます。

独立性が高いということは、逆に依存度は下がります。その結果、より汎用的で応用範囲も広がり、プログラムの価値も高くなります。

そんな独立性を高めるための機能をご紹介します。(やっとですね!)
C++では、オブジェクトの独立性をどうやって確保するのでしょうか。前回のサンプルコードをもう一度見てみましょう。

コード1

struct Point  // データと処理が一体となっている構造体
{
    int m_x, m_y;
    void set(int x, int y);
};

// Pointのメンバ関数
void Point::set(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.set( 100, -200 );
    reutrn 0;
}
実はこのコードには問題があります。それは、次のようにメンバ変数を直接どこからでもアクセスできてしまうところです。

コード2

struct Point  // データと処理が一体となっている構造体
{
    int m_x, m_y;
    void set(int x, int y);
};

// Pointのメンバ関数
void Point::set(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.m_x = 100;  // C言語と同じように直接メンバ変数を変更できてしまう。
    p.m_y = -200;
    reutrn 0;
}
現実的にこのような単純な構造体でしたら直接的な問題はないのかも知れません。
しかし、たとえば分数を表す構造体で、分母に0を設定できないというルールがあった場合はどうでしょうか。

コード3

struct Fraction  // 分数
{
    int      m_numerator;    // 分子
    unsigned m_denominator;  // 分母
    bool   set(int numerator, unsigned denominator);
    double getDouble();
};

// メンバ関数(戻り値は成功/不成功)
bool Fraction::set(int numerator, unsigned denominator)
{
    if(denominator == 0) // 分母に0は設定できない
        return false;    // エラーを返す

    m_numerator   = numerator;
    m_denominator = denominator;

    return true;
}

// これもメンバ関数
double Fraction::getDouble()
{
    // これは分母が0だと困ります
    return (double)m_numerator / (double)m_denominator;
}

int main()
{
    Fraction frt;
    if(! frt.set( -2, 3 ))
        return 1;

    frt.m_denominator = 0;  // ★やばい!

    printf("%f\n", frt.getDouble());  // ここで0除算で落ちるね(;;)

    reutrn 0;
}
せっかく、構造体専用の関数set()で分母が0の場合をエラーにして設定できないようにしていたのに、直接メンバ変数を操作されてしまえば意味がありません。
さて、どうしましょうか。コーディング規約に「メンバ変数を直接操作しないこと!」の一行を追加しましょうか。でも、デバッグ用の特例コードで変数を操作して、うっかりそのコードを本番に残してしまったら? 大量のコードからどうやって見つけますか? ひょっとしたら、m_denominator という変数名は他でも使っているかも知れません。すると、grep機能を使ってもなかなか見つけられませんね。

C++にはオブジェクト指向の隠蔽という考え方を反映させた機能が最初から組み込まれています。コード3にそれを適用したものがコード4です。

コード4

struct Fraction  // 分数
{
private:  // プライベート宣言をして、隠蔽する
    int      m_numerator;    // 分子
    unsigned m_denominator;  // 分母
public:   // 公開することを宣言する
    bool   set(int numerator, unsigned denominator);
    double getDouble();
};

// メンバ関数(戻り値は成功/不成功)
bool Fraction::set(int numerator, unsigned denominator)
{
    if(denominator == 0) // 分母に0は設定できない
        return false;    // エラーを返す

    m_numerator   = numerator;
    m_denominator = denominator;

    return true;
}

// これもメンバ関数
double Fraction::getDouble()
{
    // これは分母が0だと困ります
    return (double)m_numerator / (double)m_denominator;
}

int main()
{
    Fraction frt;
    if(! frt.set( -2, 3 ))
        return 1;

    frt.m_denominator = 0;  // ★★コンパイルエラー!

    printf("%f\n", frt.getDouble());

    reutrn 0;
}
構造体にキーワード「private」と「public」が追加されています。private: 以後は外部からのアクセスができないメンバ(変数も関数も)になります。(もちろんメンバ関数からは自由にアクセスできます)
public: 以後は公開メンバとなります。
その結果、main()関数の中で直接プライベートメンバに値を設定しようとするところでコンパイルエラーになってしまいました。

このように外部に晒したくないメンバを隠蔽することで、不要なアクセスをされる心配がなくなります。アクセスされないことが保証されるわけです。そのことにより構造体がオブジェクトとして独立性を高められる、ということになるのです。

他人の家には、玄関を通してのみ、許可を得て出入りするのと一緒ですね。その家の裏口も窓も開けっぱなしでは、いつ誰が入ってくるか判りません。周辺の住人を信用しているかいないかの問題ではなく、そういった疑いを持たずに済む、ということが重要なのです。

クラスの登場

ここまで、C言語にもある構造体を使って説明してきました。しかし、C++では上記の例のような場合は struct ではなく、class を使うのが一般的です。クラスというのはオブジェクト指向の用語でもあります。実は、C++では struct もクラスです。違いはただ1点だけです。
それは、デフォルトが private か public かの違いだけです。class と struct、どちらがどちらでしょうか。上記のコードを見ていただければ判ると思いますが、class はデフォルトが private、つまり、public: が現れる前は private 扱いになり、struct はその逆です。

コード5

class Fraction  // 分数クラス
{
    int      m_numerator;    // 分子(プライベートメンバ)
    unsigned m_denominator;  // 分母(プライベートメンバ)
public:
    bool   set(int numerator, unsigned denominator);
    double getDouble();
};
違いはそれだけなのですが、慣例として struct を使うのは受動的なデータ構造を扱う場合が多いです。つまり、C言語で使っていたようなデータ構造を示すだけの構造体的なものの場合ですね。

オブジェクト指向の用語でもあるクラスを示す class のデフォルトが private なのは意味があります。公開は必要最小限にして、独立性を高め、プライバシーを強く意識することが信頼性を高めるために大切だからです。

更に、家の玄関を通常は1つにするように、インタフェースはできるだけシンプルにするという意味もあります。

コラム:隠蔽とは?


隠蔽という言葉はオブジェクト指向での用語でもありますが、やや誤解されがちな言葉です。
C++では通常ヘッダファイル(*.h)に class を定義することが多いのですが、クラスを使ったライブラリ(クラスライブラリ)をバイナリで提供しようとしても、ヘッダファイルにクラスで使用している変数名が見えてしまいます。privateであっても、人には見えてしまいます。それで、隠蔽できないじゃないか、と言うわけです。

もちろん、オブジェクト指向での隠蔽は他のプログラムから見えない(アクセスできない)という意味ですから、人の目に見える見えないとは直接関係はないのです。
しかし、ライブラリを提供されて、なにかちょんぼ?をしようとして、ヘッダファイルのprivateをpublicに書き換えて変数に直接アクセスしよう、と思えばできてしまうかも知れません。
それを避ける方法はC++自体にはありませんが、プライベートメンバーは void *だけにして、そこに実行時に意味のあるオブジェクトを動的に割り当てる、といったやり方等でやれないことはないでしょう。
しかし、現実的にそれが必要になることはほとんど無いようには思います。あるとすれば、かなり特殊な事例だと思われます。

0 件のコメント:

コメントを投稿