はじめに

このページではプログラミングに関するメモを掲載しています。

現在掲載している他にも注意しておきたい点がありましたらどんどん編集していってください。

C++におけるポインタのポインタについて

ポインタのポインタって何なの?なんでそんなもの使うの?バカなの? という声を複数聞いたので、ちょろっと解説してみようと思います。

  • ポインタとアドレスの違い
  • ポインタのポインタとは
  • 何故それを使うのか

の3本立てにするので、分かってる所は読み飛ばしても良いです。 最初の違いについては、この説明文読む上での前提を示すので、分かってても一応読んだ方がいいかもしれません。

ポインタとアドレスの違い

この説明ではポインタとアドレスという言葉を区別して用います。 よくポインタ=アドレスと解釈してる人もいるのですが、それはある意味正しく、ある意味間違いです。C/C++ではそれで正しいのですが、言語によっては必ずしもそうではないためです(Javaとか)。

ポインタの本来の意味は「変数やインスタンスを一意に指し示す(個体識別する)もの」であり、一意に指し示すために具体的に何が使われるかは言語や処理系により様々です。C/C++ではメモリアドレスが使われますが、ユニークなIDによってインスタンスを識別する言語もあります。

私自身がC++erであるので、この説明もC++を前提として述べます。 その上で、次のように単語を使い分けます。

  • アドレス:変数やインスタンスを一意に指し示す値
  • ポインタ:アドレスを格納するための変数

一般的な変数における値と変数の関係だと思ってください。 アドレスが値で、それを代入するための変数がポインタです。

ポインタのポインタとは

通常の変数からアドレスを取得し、ポインタに代入するには次のようにします。

    int a = 10;   // 普通のint型の変数を用意
    int *p = &a;  // aのアドレスをポインタpに代入

ポインタも変数の一種ですので、当然ポインタ自体にもアドレスがあることになります。 今pの値にはaのアドレスが入っていますが、それではなくてポインタp自体のアドレスを取り出して代入するには次のようにします。

    int **pp = &p; // p自体のアドレスをポインタのポインタppに代入

この時のppが「ポインタのポインタ」です。 ポインタに入っている値ではなく、ポインタ自体のアドレスを指していることに注意しましょう。

何故それを使うのか

私が把握している限り、ポインタのポインタを用いる理由は2つ考えられます。

1つは「2次元配列」です。 m行n列のような配列だったら1次元配列で事足りますが、行毎に列の長さが異なるような配列の場合は、ポインタのポインタ(ポインタの配列、と呼んだ方が分かりやすいかも)を用いた方がスマートで効率的です。 これについては説明しているサイトが他にもありますので、そちらに説明は譲ります。 (参考サイト→ http://www2.netf.org/pointer4.html )

もう1つは「アドレスの代入を強制させるため」です。これがこの説明のメインです。 いきなり斬り込むのではなく、前段として例を挙げます。

ある計算結果をint型の値で返してくる関数があるとします。 とても大事な計算結果なので、関数を作った人としては「必ずちゃんと変数に代入して、捕まえて欲しいなぁ」と思っているものとします。 ですが、関数を呼び出す時には返値を必ず代入しなければならないという決まりはありません。 関数の作成者の祈りも空しく、次のようなコードを書かれてしまうことはあり得ます。

    hoge(); // 本当は大事な結果を返すのに……

これを防ぐために、関数hoge()を次のように作りかえました。

void hoge(int *p)
{
    *p = 1919; // とても大事な計算結果をpが指す変数に代入
}

このように作りかえた場合、関数hoge()は次のようにしないと呼べません。

    int a = 0;
    hoge(&a);

int型のポインタを引数に取ることで、必ず変数のアドレスを渡さなければいけなくなりました。 返値の場合は値を受け取ってもらえる保証はありませんでしたが、こうすることで確実に値を変数に代入できるわけです。

さて、本題です。 あるクラスHogeのインスタンスを手続きに沿って生成し、そのアドレスを返す処理を関数化したいケースを考えます。その関数以外ではnew Hoge()をするのは禁止されているとします。 シンプルに考えるのならば、

Hoge * makeHoge()
{
    return new Hoge();
}

こんな関数を作って、受け止めてもらえたらいいと思うでしょう。 ですが、

    makeHoge(); // リーク確定切腹モノコード

こういうコードを書かれたら身もフタもありません。 (Hogeクラス側でインスタンスを管理するような仕組みを導入してれば話は別ですけども。) これを防ぐために、前段で述べた仕組みを導入します。

void makeHoge(Hoge **ppH)
{
    *ppH = new Hoge(); // ppHが指しているポインタに、newして得られたアドレスを代入
}

Hogeクラスのポインタのポインタを、引数として取るようにします。 なので、makeHoge()にはHogeクラスのポインタのアドレスを渡さないと呼べなくなります。

    Hoge *pH = NULL;
    makeHoge(&pH);

こうすれば、newして得られたアドレスを必ずポインタに代入することになりますので、リークの危険性は下がるだろう、というのがこの設計の狙いだと思われます。

とまぁ長々と説明しましたが、ポインタへの代入に強制力を持たせても、他にリークする要因はいくらでも考えられるので、あまり効果的ではないのではないかと個人的には思います。私は実際の設計方針ではまず使いませんし、極力回避します。

ダブルポインタがこういう目的で頻出するのは、DirectXを扱う時が多いと思います。MicrosoftのAPIは関数の動作の正否をHRESULTで返すことが定例になっているので、単純に返値がふさがってるからダブルポインタになってる、だけなのかもしれないですが、恐らく今回述べたような目的も多分にあるのではないか、と推測します。

一見不可解な設計や自分が苦手な記法にも、必ず意図や経緯があるはずです。何故そうなっているのかを考察するのは良い経験になると思いますし、その上で自分も取り入れるかどうかを考えるなり、実践するなりしていくとスキルアップに繋がるんじゃないかと思います。


クラスのメンバに配列を持った場合、どう初期化するべきか

ここがイヤだよC++

C言語の解説サイトなどで、配列の解説の時に、

    int array[3] = {1, 2, 3};

などと記述してあるのを良く見ます。 じゃあ、これがメンバ変数になった時は、

// これは駄目コードです
class Hoge {
  private:
    int array[3];
  public:
    Hoge()
    {
        array = {1, 2, 3};
    };
}

これが許されるの?と思いたくなりますが、残念ながら駄目です。 配列を直接突っ込めるのは、その配列を実際に宣言する時だけなのです。 上記のコードだと、arrayはメンバ変数ですが、{1, 2, 3}は「その場に一時的に作られた配列」でしかないため、そもそもの寿命が釣り合いません。

じゃあ初期化リストならいけるんじゃん?と思うのは極々自然なことだと思います。 ですが、残念なことに現状のC++では策定されていない記法です。次期標準仕様候補のC++0xでは出来るみたいなので、それに期待するしかないですね。 (参考サイト http://d.hatena.ne.jp/faith_and_brave/20080718/1216371614 )

定数値の場合

じゃあどないせぇっちゅうねん!て話ですが、メンバ「定数」としたいのならば以下のような記法で行けます。

// 宣言部(h)
class Hoge {
  private:
    static const int array[3];
};

// 定義部(cpp)
// 宣言ヘッダをインクルードした直後に、どこの関数スコープにも属さない場所で
const int Hoge::array[3] = {1, 2, 3};

staticメンバは、その実体定義と初期化をクラス宣言の外部で行う必要があります。これならば配列をダイレクトに代入することが出来ます。

constを外せば配列内の値を変更することも出来ますが、あくまでstaticメンバなので全インスタンス共通の値になってしまい「各インスタンス固有の値」にはなりません。

メンバ「変数」として配列を扱いたい場合

{}で囲った配列の記法での代入にとことんこだわる場合は、以下のような記法が考えられます。

class Hoge {
  private:
    int array[3];
  public:
    Hoge()
    {
        int tmpArray[3] = {1, 2, 3};

        for(int i = 0; i < 3; i++) {
            array[i] = tmpArray[i];
        };
    };
};

配列のコピーは単純なforループで実現できるので、いったん{}記法で作成した配列からメンバ配列へコピーする、というのが新たな知識を得ずともなんとかなる方法かと思います。

こういう所がC++が泥臭いと言われる所以なんでしょうね…。


クラスのメンバを扱う処理を高速化する

このトピックは以下のような読者向けです。

  • C++コードの高速化に興味がある。
  • メンバ関数内でループによってメンバ変数(配列)を頻繁に参照する。
  • STLのvectorを多用しているが、いまいちパフォーマンスに不満がある。

どれかに該当する人は読む価値があると思います。

クラスのメンバへのアクセスは遅い!

「C++は高速である」と喧伝されることが多いですが、実際にはCのコードに変換されてからマシン語になってます。なので、C++ならでは機能や構造を使っている部分には多少コストがかかることが多いです。その最たる例が「クラスメンバの呼び出し」です。

多少の呼び出し(例えば60FPSで動くゲームで1フレームあたりに2〜3度呼び出す程度)なら気にする必要はありません。しかし数百回オーダー以上のループ処理内で、などという話になると無視できないコストになってきます。このため、ループ内では条件分岐を極力減らすのはもちろん、メンバ呼び出しも最低限に留める努力をしたいところです。

メンバ関数からメンバにアクセスするのも遅い?

前述のメンバアクセスは「インスタンス名.関数()」や「ポインタ->関数()」のことだと思われるかもしれませんが、実はクラス内で自分のメンバを参照するのにもコストがかかっています。

自分自身のメンバに対するアクセスは、暗黙的に「this->メンバ関数()」や「this->メンバ変数」のように置き換えられています。なのでこれも「メンバアクセス」と見なされるのです。このため、クラス内にパージした処理は意識しないと全てコストを積み増しされた状態になってしまうのです。

じゃあどうするか?「けんごりん展開」

こういう時の対処方法として、メンバ変数の値やポインタを「ローカルにコピーしてきてアクセスする」というテクニックがあります。発見者であり伝授者の名前を取って、けんごりん展開と呼ぶことにします。

マクローリン展開というのがあってですね、と野暮な解説。

メンバ関数内のローカル変数ならば、Cのプログラムと変わらないコストでアクセスすることになるため、余計なコストを掛けずに済みます。処理内容にもよりますが、特に大量のデータを扱うループでは効果的なようです。

例を挙げます。次のようなクラスHogeがあるとします。

class Hoge {
  private:
    double *mArray;
    int    mLoopNum;
    double mTh;
  public:
    Hoge(void)
    {
        mLoopNum = 1000000;
        mArray = new double[mLoopNum];
        mTh = mLoopNum / 2.0;
    };
    void proc(void);
};

メンバとしてdoubleの配列mArrayを持ち、その配列のサイズをmLoopNumに格納しています。mThはダミーの条件分岐用の値なのであまり気にしないで下さい。そして、この配列に対して処理を行う以下のようなメンバ関数proc()があるとします。

void Hoge::proc(void)
{
    for(int i = 0; i < mLoopNum; i++) {
        mArray[i] = 1.0 * i;
        if(mArray[i] > mTh) {
            mArray[i] /= 2.0;
        } else {
            mArray[i] *= 2.0;
        }
    }
}

ループ内で条件分岐を伴う配列アクセスを行っています。ループの条件判定にメンバ変数を使っているため、これも地味にコストを喰う要因になっています。これを以下のように直します。

void Hoge::proc(void)
{
    register int    loLoopNum = mLoopNum;
    register double *loArray = mArray;
    register double loTh = mTh;
    for(int i = 0; i < loLoopNum; i++) {
        loArray[i] = 1.0 * i;
        if(loArray[i] > loTh) {
            loArray[i] /= 2.0;
        } else {
            loArray[i] *= 2.0;
        }
    }
}

ローカル変数宣言時のregisterという修飾子は、レジスタメモリという高速なメモリに変数を割り当てることを指示するものです。必ず効果があるわけではありませんが、局所的な変数では宣言しておくと良いかもしれません。

このクラスを実際に利用したコードでは、CPUにもよりますがおよそ25%の高速化を確認できました。有意差と見て良いと思います。気をつけて欲しいのは、参照渡しでは効果が無いことです。あくまで値渡し(配列に対する操作も先頭アドレスをコピーしているだけです)にしないと意味がありません。

vectorも遅いんじゃね?

C++では配列の確保は全てvectorに任せっきり、という人もいると思います。当然クラスのメンバにすることもあるでしょう。しかし、メンバアクセスによるコストの影響を受けるのはもちろん、vector配列へのランダムアクセス([]やat()によるアクセス)自体にもコストがかかります。もちろん通常のコンテナに比べれば速い方ではありますが、要素数がかさんでくると例によって例の如く無視できなくなってきます。

vector配列には以下の特徴があります。

  • 配列として確保されているメモリ空間は、通常の配列と同様に連続することが保証されている。
  • 配列のサイズ変更を伴う操作(push_back()やerase(),resize()など)を行うと、メモリ空間ごと引っ越しを行う可能性がある。

これらから言えるのは「vector配列の先頭要素のアドレスを取得してポインタとして扱うことで、サイズ変更を行わない限りは通常の配列と同じ用法、コストでアクセスできる」ということです。なので速度が欲しい場面ではそのようにしましょう。

HogeクラスのmArrayがvector<double>だったとした場合、前述したメンバ関数proc()は以下のように書き換えれば高速なアクセスが実現できます。

void Hoge::proc(void)
{
    register int    loLoopNum = mLoopNum;
    register double *loArray = &(mArray[0]);
    register double loTh = mTh;
    for(int i = 0; i < loLoopNum; i++) {
        loArray[i] = 1.0 * i;
        if(loArray[i] > loTh) {
            loArray[i] /= 2.0;
        } else {
            loArray[i] *= 2.0;
        }
    }
}

vector配列からローカルポインタに展開した場合の効果は凄まじく、実験では66%程度の高速化が確認できました。これは導入すると嬉しい場面が多そうです。vectorコンテナ自体のポインタ(vector<double>*)をローカルに持ってきても、高速化するどころか逆に絶望的な遅さになる(->at(i)が非常に高コストなため)ので注意しましょう。

まとめと補足

  • ループ処理でメンバを扱う際はローカル変数やローカルポインタに展開しよう
  • vector配列は単純な参照、代入を行う場面ではローカルポインタに展開しよう
  • 画像や点群など、ループがかさむ場面でないと効果がないので過度な期待はせず、事前調査をしてから導入しよう

前述した処理を500回繰り返して所要時間を計測しました。コンパイラはVisual C++ 2008、CPUはCore2Duo U9300(1.2GHz)です。

''配列種別''''アクセス方法''''所要時間''
生配列メンバ3.4秒
生配列ローカル2.6秒
vectorメンバ6.2秒
vectorローカル2.6秒
vectorイテレータ13.2秒

本来vector配列はSTLの作法に沿って利用する場合はイテレータを用いることが望ましいです。イテレータについてはこちらの資料に詳しいので、よく分からない人は参照して下さい。

上記の結果だと残念な結果が出ていますが、実はVisualStudioにおけるvector配列の実装はとにかく安全性を重視しており、そのままでは本来のパフォーマンスが出ないようになっています。このリミットを解除する方法があります。

#define _SECURE_SCL 0

という定義をソースファイルの冒頭に記述しておくと、リミットが解除されてSTL全般の動作が大幅に向上します。この場合の計測結果は次のようになります。

''配列種別''''アクセス方法''''所要時間''
生配列メンバ3.4秒
生配列ローカル2.6秒
vectorメンバ3.7秒
vectorローカル2.6秒
vectorイテレータ3.2秒

このようにリミットを解除した状態では、速度と安全性、機能性のバランスを考えるとイテレータによるアクセスが有利になります。用途や誰か目にするコードなのかなどを踏まえて選択するとよいでしょう。

この記事をまとめるきっかけをくださった、けんごりんに感謝します。 また、イテレータの有用性を説いてくれたmiki_high君にも感謝します。

おまけ

ローカルスコープではメンバ変数と同じ名前を宣言しても区別されるため、それを利用してこんなマクロが書けます。

#define MEM2LOC(type, name) type name = this->name;

これを使うと、メンバ関数内の処理部を書き換えずに高速化が図れます。

void Hoge::proc(void)
{
    MEM2LOC(int, mLoopNum);
    MEM2LOC(double *, mArray);
    MEM2LOC(double, mTh);
    for(int i = 0; i < mLoopNum; i++) {
        mArray[i] = 1.0 * i;
        if(mArray[i] > mTh) {
            mArray[i] /= 2.0;
        } else {
            mArray[i] *= 2.0;
        }
    }
}

vector配列には別途マクロを作る必要がありますが、その他の値渡しが可能な変数に関してはこれで大丈夫だと思います。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2011-05-19 (木) 14:24:49 (2286d)