コンパイラーの検査をかいくぐる


const宣言したいのに、コンパイラーがそうさせてくれない ことがあります。これが困る。

困った例(1)

よくあるのは、使いたいライブラリーに適切なconstがついていない場合。 例えば、

        void Hoge::Open(const char * filePath)
        {
                ::LibOpen(filePath);
                // 以下省略
        }

という具合に、誰かが作ったライブラリーの ::LibOpen() を使って処理するとします。 引数の filePath は定数である(つまり、Hoge::Open() の中で変化しない)と いうことで、constを付けています。 ところが、LibOpen() の第1引数にconstがついていないと、これはエラーになります。

Hoge::Open() の引数からconstを取ればコンパイラーに文句を言われないのは わかっている。でもこういう場合、このライブラリーの函数はことごとく constなしで作られていたりします。ということは、あちこちで本当はつけたい constを削らなければならなくなり、constの効用が活かされません。

困ったので

        void Hoge::Open(const char * filePath)
        {
                char *  path = ::strdup(filePath);
                ::LibOpen(path);
                // 以下省略
        }

としてみたら、今度は strdup() の引数にconstがなくて、こちらがエラーになった。

        void Hoge::Open(const char * filePath)
        {
                char *  path = ::malloc(strlen(filePath) + 1);
                ::strcpy(path, filePath);
                ::LibOpen(path);
                // 以下省略
        }

も同様の理由で不可。結局、string.h や memory.h の函数を一切使わず、

        void Hoge::Open(const char * filePath)
        {
                char    path[MAX_PATH + 1];
                int     i;
                for (i = 0; filePath[i] != '\0'; i++) {
                        path[i] = filePath[i];
                }
                path[i] = '\0';
                ::LibOpen(path);
                // 以下省略
        }

みたいなことになりました。

あ、最近はstring.hやmemory.hにはちゃんとconstがついているのが多いので、 ここまでトホホな状況にはなりませんけど。

似た例としては、ライブラリーの方で

        typedef struct  {
                char *          path;
                // 省略
        } PARAM;
        extern  void    LibOpen(PARAM * param);

みたいに宣言されていて、

        void Hoge::Open(const char * filePath)
        {
                PARAM           param;
                param.path = filePath;
                ::LibOpen(&param);
                // 以下省略
        }

というように使いたい場合。これは代入文でエラーが出てしまいますから、 エラーの意味が前よりもわかりにくくなります。

困った例(2)

ライブラリーを使わず、自分ですべて書いていても困ることがあります。 例えば、最初こんな函数を作りました。

        == hoge.h ==
        class Hoge
        {
        public:
                virtual int GetMunya(void) const;
        };
        == hoge.cpp ==
        int Hoge::GetMunya(void) const
        {
                int     munya = /* いろいろ計算 */;
                return munya;
        }

ところが、munyaの「いろいろ計算」の中身は本当にいろいろで、とても時間がかかる。 しかもHoge::GetMunya()はしょっちゅう使われる函数だということで、こんな風に 改良してみます。

        == hoge.h ==
        class Hoge
        {
        public:
                virtual int GetMunya(void) const;
        private:
                boolean MunyaCalced;
                int     Munya;
        };
        == hoge.cpp ==
        int Hoge::GetMunya(void) const
        {
                if (! MunyaCalced) {
                        Munya = /* いろいろ計算 */;
                        MunyaCalced = true;
                }
                return Munya;
        }

もちろん、MunyaCalcedは構築子あたりでfalseに初期化されていると思ってください。 こうすると、時間がかかる「いろいろ計算」は1回だけで済みます。

問題は、Hoge::GetMunya()で変数MunyaやMunyaCalcedが更新されるため、 constがついているとエラーになってしまうこと。 でも、実装が変わったからconstがつけられなくなるというのも理不尽な気がします。


何が問題か

結局問題はコンパイラーが実装の検査をしているにすぎないところにあるんですね。 キャッシングのための代入文も、本当にオブジェクトの状態を変える代入文も、 区別がつかないのです。 もちろんコンパイラーにプログラムの「意図」を汲み取れという訳には いきませんし、実装の検査以上に余計なことをされてはもっと困ってしまいます。

最近のコンパイラーにはこういう問題を回避するためのしかけがあります。 それが更に面倒を引き起こしたりもするのですが、この辺の話はまた次回。